use super::*;
use crate::solver::estimate::{DP_FLOOR, smooth_floor_dp};
use approx::assert_relative_eq;
use ndarray::array;
#[test]
pub(crate) fn trace_matrix_product_iterator_matches_scalar_reference_bitwise() {
let left = Array2::from_shape_fn((4, 4), |(i, j)| {
((i as f64 + 0.25) * 0.37 + (j as f64 + 0.5) * 0.19).sin()
});
let right = Array2::from_shape_fn((4, 4), |(i, j)| {
((i as f64 + 1.25) * 0.23 - (j as f64 + 0.75) * 0.41).cos()
});
let mut reference = 0.0;
for i in 0..left.nrows() {
for j in 0..left.ncols() {
reference += left[[i, j]] * right[[j, i]];
}
}
assert_eq!(
trace_matrix_product(&left, &right).to_bits(),
reference.to_bits()
);
}
#[test]
pub(crate) fn block_local_bilinear_iterator_matches_scalar_reference_bitwise() {
let local = Array2::from_shape_fn((4, 4), |(i, j)| {
((i as f64 + 0.1) * 0.29 - (j as f64 + 0.7) * 0.13).sin()
});
let op = BlockLocalDrift {
local: local.clone(),
start: 2,
end: 6,
total_dim: 8,
};
let v = Array1::from_shape_fn(8, |i| ((i as f64 + 0.4) * 0.31).cos());
let u = Array1::from_shape_fn(8, |i| ((i as f64 + 0.8) * 0.17).sin());
let mut reference = 0.0;
for row in 0..local.nrows() {
let mut row_dot = 0.0;
for col in 0..local.ncols() {
row_dot += local[[row, col]] * v[op.start + col];
}
reference += u[op.start + row] * row_dot;
}
let got = op.bilinear_view(v.view(), u.view());
assert_eq!(got.to_bits(), reference.to_bits());
}
#[test]
pub(crate) fn xt_logdet_kernel_diagonal_iterator_matches_scalar_reference() {
struct FixedKernelHessian {
pub(crate) kernel: Array2<f64>,
}
impl HessianOperator for FixedKernelHessian {
fn logdet(&self) -> f64 {
0.0
}
fn trace_hinv_product(&self, a: &Array2<f64>) -> f64 {
assert_eq!(a.raw_dim(), self.kernel.raw_dim());
let mut trace = 0.0;
for i in 0..a.nrows() {
for j in 0..a.ncols() {
trace += self.kernel[[i, j]] * a[[j, i]];
}
}
trace
}
fn solve(&self, rhs: &Array1<f64>) -> Array1<f64> {
self.kernel.dot(rhs)
}
fn solve_multi(&self, rhs: &Array2<f64>) -> Array2<f64> {
self.kernel.dot(rhs)
}
fn active_rank(&self) -> usize {
self.kernel.nrows()
}
fn dim(&self) -> usize {
self.kernel.nrows()
}
}
let x = Array2::from_shape_fn((5, 3), |(i, j)| {
((i as f64 + 0.5) * 0.21 + (j as f64 + 0.25) * 0.47).sin()
});
let kernel = array![[1.7, 0.2, -0.1], [0.2, 2.3, 0.4], [-0.1, 0.4, 1.9]];
let op = FixedKernelHessian { kernel };
let design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x.clone()));
let got = HessianOperator::xt_logdet_kernel_x_diagonal(&op, &design);
let solved = op.solve_multi(&x.t().to_owned());
for i in 0..x.nrows() {
let mut reference = 0.0;
for j in 0..x.ncols() {
reference += x[[i, j]] * solved[[j, i]];
}
assert_eq!(got[i].to_bits(), reference.to_bits());
}
}
#[test]
pub(crate) fn xt_projected_kernel_diagonal_iterator_matches_scalar_reference_bitwise() {
let u_s = array![[0.8_f64, -0.2], [0.1, 0.9], [0.5, 0.3], [-0.4, 0.6]];
let h_proj_inverse = array![[1.6_f64, -0.25], [-0.25, 2.1]];
let subspace = PenaltySubspaceTrace {
u_s: u_s.clone(),
h_proj_inverse: h_proj_inverse.clone(),
};
let x = Array2::from_shape_fn((5, 4), |(i, j)| {
((i as f64 + 0.3) * 0.19 - (j as f64 + 0.6) * 0.37).sin()
});
let design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x.clone()));
let got = subspace.xt_projected_kernel_x_diagonal(&design);
let z = crate::faer_ndarray::fast_ab(&x, &u_s);
for i in 0..x.nrows() {
let row_z = z.row(i);
let mut reference = 0.0;
for a in 0..h_proj_inverse.nrows() {
let mut inner = 0.0;
for b in 0..h_proj_inverse.ncols() {
inner += h_proj_inverse[[a, b]] * row_z[b];
}
reference += row_z[a] * inner;
}
assert_eq!(got[i].to_bits(), reference.to_bits());
}
}
#[test]
pub(crate) fn projected_logdet_cross_reduced_uses_trace_product_reference() {
let kernel = PenaltySubspaceTrace {
u_s: Array2::<f64>::eye(3),
h_proj_inverse: array![[1.4, 0.2, -0.1], [0.2, 1.9, 0.3], [-0.1, 0.3, 1.6]],
};
let ra = Array2::from_shape_fn((3, 3), |(i, j)| {
((i as f64 + 0.6) * 0.22 - (j as f64 + 0.3) * 0.35).sin()
});
let rb = Array2::from_shape_fn((3, 3), |(i, j)| {
((i as f64 + 0.1) * 0.41 + (j as f64 + 0.8) * 0.18).cos()
});
let left = kernel.h_proj_inverse.dot(&ra);
let right = kernel.h_proj_inverse.dot(&rb);
let mut reference = 0.0;
for i in 0..left.nrows() {
for j in 0..left.ncols() {
reference += left[[i, j]] * right[[j, i]];
}
}
let got = kernel.trace_projected_logdet_cross_reduced(&ra, &rb);
assert_eq!(got.to_bits(), reference.to_bits());
}
#[test]
pub(crate) fn dense_spectral_rotated_cross_kernels_match_scalar_references_bitwise() {
let h = array![[3.5, 0.4, -0.2], [0.4, 2.8, 0.3], [-0.2, 0.3, 2.2]];
let op = DenseSpectralOperator::from_symmetric(&h).expect("spd fixture");
let a_rot = Array2::from_shape_fn((3, 3), |(i, j)| {
((i as f64 + 0.2) * 0.31 + (j as f64 + 0.9) * 0.27).sin()
});
let b_rot = Array2::from_shape_fn((3, 3), |(i, j)| {
((i as f64 + 0.7) * 0.17 - (j as f64 + 0.4) * 0.43).cos()
});
let mut hinv_reference = 0.0;
let mut logdet_reference = 0.0;
let mut projected_reference = 0.0;
for i in 0..op.n_dim {
for j in 0..op.n_dim {
hinv_reference += op.hinv_cross_kernel[[i, j]] * a_rot[[i, j]] * b_rot[[j, i]];
logdet_reference += op.logdet_hessian_kernel[[i, j]] * a_rot[[i, j]] * b_rot[[j, i]];
projected_reference += a_rot[[i, j]] * b_rot[[j, i]];
}
}
assert_eq!(
op.trace_hinv_product_cross_rotated(&a_rot, &b_rot)
.to_bits(),
hinv_reference.to_bits()
);
assert_eq!(
op.trace_logdet_hessian_cross_rotated(&a_rot, &b_rot)
.to_bits(),
logdet_reference.to_bits()
);
assert_eq!(
op.trace_projected_cross(&a_rot, &b_rot).to_bits(),
projected_reference.to_bits()
);
}
#[test]
pub(crate) fn batched_penalty_subspace_traces_match_exact_kernel_on_ill_conditioned_spectrum() {
let p = 6usize;
let r = 4usize;
let mut u_s = Array2::<f64>::zeros((p, r));
for col in 0..r {
for row in 0..p {
let x = ((row * r + col) as f64 * 0.7311).sin();
u_s[[row, col]] = x;
}
}
for col in 0..r {
for prev in 0..col {
let dot = u_s.column(col).dot(&u_s.column(prev));
let prev_col = u_s.column(prev).to_owned();
let mut c = u_s.column_mut(col);
c.scaled_add(-dot, &prev_col);
}
let norm = u_s.column(col).dot(&u_s.column(col)).sqrt();
u_s.column_mut(col).mapv_inplace(|v| v / norm);
}
let sigmas = [1.0e-6_f64, 1.0e-2, 1.0e2, 1.0e6];
let mut m = Array2::<f64>::zeros((r, r));
for (a, &s) in sigmas.iter().enumerate() {
m[[a, a]] = 1.0 / s;
}
let kernel = PenaltySubspaceTrace {
u_s: u_s.clone(),
h_proj_inverse: m,
};
let u3 = u_s.column(r - 1).to_owned();
let mut a_drift = Array2::<f64>::zeros((p, p));
for i in 0..p {
for j in 0..p {
a_drift[[i, j]] = 1.0e6 * u3[i] * u3[j]
+ 0.5 * (((i + 2 * j) as f64) * 0.3719).cos()
+ 0.5 * (((j + 2 * i) as f64) * 0.3719).cos();
}
}
let drifts = vec![DriftDerivResult::Dense(a_drift.clone())];
let batched = penalty_subspace_trace_drifts_batched(&kernel, &drifts);
let exact = kernel.trace_projected_logdet(&a_drift);
assert!(
(batched[0] - exact).abs() <= 1e-10 * (1.0 + exact.abs()),
"batched kernel trace must reproduce the exact per-coordinate \
contraction on an ill-conditioned spectrum: batched={} exact={}",
batched[0],
exact
);
}
#[test]
pub(crate) fn guarded_correction_include_false_applies_neither() {
let value = 3.5_f64;
let gradient = array![0.25, -0.75, 1.5];
let correction =
GuardedCorrection::new(value, Some(gradient.clone()), false);
let mut cost_split = 10.0;
let mut grad_split = array![0.0, 0.0, 0.0];
correction.apply_value(&mut cost_split);
correction.apply_gradient(&mut grad_split);
assert_eq!(cost_split, 10.0, "apply_value must respect include=false");
assert_eq!(
grad_split,
array![0.0, 0.0, 0.0],
"apply_gradient must respect include=false"
);
}
#[test]
pub(crate) fn guarded_correction_include_true_applies_both() {
let value = 3.5_f64;
let gradient = array![0.25, -0.75, 1.5];
let correction =
GuardedCorrection::new(value, Some(gradient.clone()), true);
let mut cost_split = 10.0;
let mut grad_split = array![0.0, 0.0, 0.0, 42.0];
correction.apply_value(&mut cost_split);
correction.apply_gradient(&mut grad_split);
assert_eq!(cost_split, 13.5);
assert_eq!(
grad_split,
array![0.25, -0.75, 1.5, 42.0],
"gradient applies to leading entries; trailing ext coords untouched"
);
}
#[test]
pub(crate) fn penalty_coord_projection_reduces_dim_and_preserves_quadratic_form() {
let root = array![
[1.0, -1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, -1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, -1.0, 0.0],
[0.0, 0.0, 0.0, 1.0, -1.0],
];
let coord = PenaltyCoordinate::DenseRoot(root.clone());
assert_eq!(coord.dim(), 5);
let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
let z = array![
[inv_sqrt2, 0.0],
[-inv_sqrt2, 0.0],
[0.0, inv_sqrt2],
[0.0, -inv_sqrt2],
[0.0, 0.0],
];
let projected = coord.project_into_subspace(&z);
assert_eq!(
projected.dim(),
z.ncols(),
"projected penalty coordinate dim must equal the reduced beta length"
);
let beta_f = array![0.7, -1.3];
let beta_full = z.dot(&beta_f);
let s_beta_full = coord.apply_penalty(&beta_full, 1.0);
let full_quadratic = beta_full.dot(&s_beta_full);
let s_beta_reduced = projected.apply_penalty(&beta_f, 1.0);
let reduced_quadratic = beta_f.dot(&s_beta_reduced);
assert_relative_eq!(reduced_quadratic, full_quadratic, max_relative = 1e-12);
}
pub(crate) fn synthetic_h_with_small_eig(
small_eig: f64,
placement: SmallEigPlacement,
) -> (Array2<f64>, Array2<f64>) {
let p = 5usize;
let r = 4usize;
let mut h_full = Array2::<f64>::zeros((p, p));
for i in 0..p {
h_full[[i, i]] = 1.0;
}
let (small_idx, u_s_cols): (usize, Vec<usize>) = match placement {
SmallEigPlacement::OutsideRangeSPlus => (4, vec![0, 1, 2, 3]),
SmallEigPlacement::InsideRangeSPlus => (0, vec![0, 1, 2, 3]),
};
h_full[[small_idx, small_idx]] = small_eig;
if matches!(placement, SmallEigPlacement::OutsideRangeSPlus) {
}
let mut u_s = Array2::<f64>::zeros((p, r));
for (col_pos, &row) in u_s_cols.iter().enumerate() {
u_s[[row, col_pos]] = 1.0;
}
(h_full, u_s)
}
#[derive(Clone, Copy)]
pub(crate) enum SmallEigPlacement {
OutsideRangeSPlus,
InsideRangeSPlus,
}
#[test]
pub(crate) fn active_projected_kkt_residual_is_reduced_before_projected_ift() {
let kernel = PenaltySubspaceTrace {
u_s: array![[1.0], [0.0]],
h_proj_inverse: array![[0.25]],
};
let active =
ProjectedKktResidual::from_active_projected(array![3.0, 1.0e-8]).with_metadata(1.0e-6, 1);
let reduced = active
.projected_into_reduced_range(&kernel)
.expect("dropped residual inside the KKT tolerance may be reduced");
assert_eq!(reduced.subspace(), KktResidualSubspace::ReducedRange);
assert_relative_eq!(reduced.as_array()[0], 3.0, epsilon = 1e-12);
assert_relative_eq!(reduced.as_array()[1], 0.0, epsilon = 1e-12);
assert_eq!(reduced.residual_tol(), Some(1.0e-6));
assert_eq!(reduced.free_rank(), Some(1));
}
#[test]
pub(crate) fn active_projected_kkt_residual_rejects_large_drop_before_projected_ift() {
let kernel = PenaltySubspaceTrace {
u_s: array![[1.0], [0.0]],
h_proj_inverse: array![[0.25]],
};
let active =
ProjectedKktResidual::from_active_projected(array![3.0, 4.0]).with_metadata(1.0e-6, 1);
let err = active
.projected_into_reduced_range(&kernel)
.expect_err("large null/range-excluded residual must not be silently dropped");
assert!(
err.contains("unresolved mass outside the reduced Hessian/penalty range"),
"unexpected error: {err}"
);
}
pub(crate) fn build_subspace_kernel(
h_full: &Array2<f64>,
u_s: &Array2<f64>,
) -> PenaltySubspaceTrace {
use crate::faer_ndarray::FaerEigh;
use faer::Side;
let h_proj = u_s.t().dot(h_full).dot(u_s);
let (evals, evecs) = h_proj
.eigh(Side::Lower)
.expect("h_proj eigh in test fixture");
let r = h_proj.nrows();
let mut h_proj_inverse = Array2::<f64>::zeros((r, r));
for k in 0..evals.len() {
assert!(
evals[k].abs() > 1e-10,
"test fixture must keep H_proj non-singular; got eval[{k}] = {}",
evals[k]
);
let inv = 1.0 / evals[k];
for i in 0..r {
for j in 0..r {
h_proj_inverse[[i, j]] += inv * evecs[[i, k]] * evecs[[j, k]];
}
}
}
PenaltySubspaceTrace {
u_s: u_s.clone(),
h_proj_inverse,
}
}
pub(crate) fn full_h_inv_bilinear(h_full: &Array2<f64>, a: &Array1<f64>, b: &Array1<f64>) -> f64 {
let p = h_full.nrows();
let mut acc = 0.0_f64;
for i in 0..p {
assert!(h_full[[i, i]].abs() > 0.0);
acc += a[i] * b[i] / h_full[[i, i]];
}
acc
}
#[test]
pub(crate) fn ift_projected_pseudo_inverse_kills_null_subspace_noise() {
assert!(file!().ends_with(".rs"));
let small_eig = 1e-12_f64;
let (h_full, u_s) = synthetic_h_with_small_eig(small_eig, SmallEigPlacement::OutsideRangeSPlus);
let kernel = build_subspace_kernel(&h_full, &u_s);
let r_honest = array![1.0_f64, 0.0, 0.0, 0.0, 0.0];
let r_noise = array![0.0_f64, 0.0, 0.0, 0.0, 1e-3];
let r_total = &r_honest + &r_noise;
let a_k = array![0.0_f64, 1.0, 0.0, 0.0, 0.0];
let corr_honest = kernel.bilinear_pseudo_inverse(&r_honest, &a_k);
let corr_proj = kernel.bilinear_pseudo_inverse(&r_total, &a_k);
let corr_full = full_h_inv_bilinear(&h_full, &r_total, &a_k);
assert_relative_eq!(corr_proj, corr_honest, max_relative = 1e-12);
assert_relative_eq!(corr_full, corr_honest, max_relative = 1e-12);
}
#[test]
pub(crate) fn ift_full_h_solve_amplifies_null_subspace_noise_by_inverse_small_eig() {
let small_eig = 1e-12_f64;
let (h_full, u_s) = synthetic_h_with_small_eig(small_eig, SmallEigPlacement::OutsideRangeSPlus);
let kernel = build_subspace_kernel(&h_full, &u_s);
let eta = 1e-3_f64; let xi = 1e-3_f64; let r = array![0.0_f64, 0.0, 0.0, 0.0, eta];
let a_k = array![0.0_f64, 0.0, 0.0, 0.0, xi];
let corr_proj = kernel.bilinear_pseudo_inverse(&r, &a_k);
let corr_full = full_h_inv_bilinear(&h_full, &r, &a_k);
assert!(
corr_proj.abs() < 1e-15,
"projected correction must drop pure-null-subspace contributions, got {corr_proj:.3e}"
);
let expected_full = eta * xi / small_eig;
assert_relative_eq!(corr_full, expected_full, max_relative = 1e-12);
assert!(
corr_full / 1.0 >= 1e5,
"full-H solve must produce a large blow-up for this fixture"
);
}
#[test]
pub(crate) fn ift_projected_pseudo_inverse_cannot_help_when_small_eig_lives_inside_range_s_plus() {
assert!(file!().ends_with(".rs"));
let small_eig = 1e-8_f64;
let (h_full, u_s) = synthetic_h_with_small_eig(small_eig, SmallEigPlacement::InsideRangeSPlus);
let kernel = build_subspace_kernel(&h_full, &u_s);
let eta = 1e-3_f64;
let xi = 1e-3_f64;
let r = array![eta, 0.0, 0.0, 0.0, 0.0]; let a_k = array![xi, 0.0, 0.0, 0.0, 0.0];
let corr_proj = kernel.bilinear_pseudo_inverse(&r, &a_k);
let corr_full = full_h_inv_bilinear(&h_full, &r, &a_k);
let expected = eta * xi / small_eig;
assert_relative_eq!(corr_proj, expected, max_relative = 1e-12);
assert_relative_eq!(corr_full, expected, max_relative = 1e-12);
}
#[test]
pub(crate) fn ift_projected_pseudo_inverse_matches_full_h_on_well_conditioned_fixture() {
assert!(file!().ends_with(".rs"));
let p = 5usize;
let r_subspace = 4usize;
let mut h_full = Array2::<f64>::zeros((p, p));
for i in 0..p {
h_full[[i, i]] = (i as f64 + 1.0) * 2.0;
}
let mut u_s = Array2::<f64>::zeros((p, r_subspace));
for j in 0..r_subspace {
u_s[[j, j]] = 1.0;
}
let kernel = build_subspace_kernel(&h_full, &u_s);
let r = array![0.3_f64, -0.7, 1.2, 0.4, 0.0]; let a_k = array![0.5_f64, 0.1, -0.2, 0.8, 0.0];
let corr_proj = kernel.bilinear_pseudo_inverse(&r, &a_k);
let corr_full = full_h_inv_bilinear(&h_full, &r, &a_k);
assert_relative_eq!(corr_proj, corr_full, max_relative = 1e-12);
}
pub(crate) fn dense_h_inv_bilinear_via_eig(
h_full: &Array2<f64>,
a: &Array1<f64>,
b: &Array1<f64>,
) -> f64 {
use crate::faer_ndarray::FaerEigh;
let (evals, evecs) = h_full
.eigh(faer::Side::Lower)
.expect("eigendecomp of test fixture H");
let ua = evecs.t().dot(a);
let ub = evecs.t().dot(b);
let mut acc = 0.0_f64;
for i in 0..evals.len() {
assert!(
evals[i].abs() > 0.0,
"fixture eigenvalue must be nonzero for direct solve"
);
acc += ua[i] * ub[i] / evals[i];
}
acc
}
pub(crate) fn projected_pseudo_inverse_truth(
h_full: &Array2<f64>,
u_s: &Array2<f64>,
a: &Array1<f64>,
b: &Array1<f64>,
) -> f64 {
use crate::faer_ndarray::FaerEigh;
use faer::Side;
let proj_a = u_s.t().dot(a);
let proj_b = u_s.t().dot(b);
let h_proj = u_s.t().dot(h_full).dot(u_s);
let (evals, evecs) = h_proj.eigh(Side::Lower).expect("h_proj eigh");
let ua = evecs.t().dot(&proj_a);
let ub = evecs.t().dot(&proj_b);
let mut acc = 0.0_f64;
for i in 0..evals.len() {
assert!(evals[i].abs() > 1e-10, "h_proj eigenvalue must be nonzero");
acc += ua[i] * ub[i] / evals[i];
}
acc
}
#[test]
pub(crate) fn ift_projected_pseudo_inverse_saves_orders_of_magnitude_on_cross_coupled_h() {
let small_eig = 1e-12_f64;
let p = 5usize;
let r_subspace = 4usize;
let mut u_s = Array2::<f64>::zeros((p, r_subspace));
for j in 0..r_subspace {
u_s[[j, j]] = 1.0;
}
let v_min = {
let leg_s = 0.15_f64;
let leg_n = (1.0 - 4.0 * leg_s * leg_s).sqrt();
array![leg_s, leg_s, leg_s, leg_s, leg_n]
};
let ambients = [
array![1.0_f64, 0.3, -0.2, 0.5, 0.0],
array![0.4_f64, 1.0, 0.6, -0.3, 0.0],
array![-0.5_f64, 0.2, 1.0, 0.7, 0.0],
array![0.6_f64, -0.4, 0.3, 1.0, 0.0],
];
let mut q = Array2::<f64>::zeros((p, p));
q.column_mut(p - 1).assign(&v_min);
let mut col_idx = 0usize;
for ambient in ambients.iter() {
let mut v = ambient.clone();
let dot = v.dot(&v_min);
v.scaled_add(-dot, &v_min);
for prev in 0..col_idx {
let qprev = q.column(prev).to_owned();
let d = v.dot(&qprev);
v.scaled_add(-d, &qprev);
}
let norm = v.dot(&v).sqrt();
assert!(
norm > 1e-10,
"Gram-Schmidt failed at col {col_idx}: norm = {norm}"
);
v /= norm;
q.column_mut(col_idx).assign(&v);
col_idx += 1;
}
assert_eq!(col_idx, p - 1);
let eigvals = array![10.0_f64, 5.0, 2.0, 1.0, small_eig];
let mut h_full = Array2::<f64>::zeros((p, p));
for i in 0..p {
let qi = q.column(i).to_owned();
for a in 0..p {
for b in 0..p {
h_full[[a, b]] += eigvals[i] * qi[a] * qi[b];
}
}
}
for a in 0..p {
for b in (a + 1)..p {
let avg = 0.5 * (h_full[[a, b]] + h_full[[b, a]]);
h_full[[a, b]] = avg;
h_full[[b, a]] = avg;
}
}
let h_proj_check = u_s.t().dot(&h_full).dot(&u_s);
let mut max_offdiag = 0.0_f64;
let mut max_diag = 0.0_f64;
for i in 0..r_subspace {
max_diag = max_diag.max(h_proj_check[[i, i]].abs());
for j in 0..r_subspace {
if i != j {
max_offdiag = max_offdiag.max(h_proj_check[[i, j]].abs());
}
}
}
assert!(
max_offdiag > 0.1 * max_diag,
"fixture must produce non-diagonal h_proj; max_offdiag = \
{max_offdiag:.3e}, max_diag = {max_diag:.3e}"
);
let kernel = build_subspace_kernel(&h_full, &u_s);
let a_k = array![0.5_f64, 0.7, -0.3, 0.9, 0.0];
let eps_null = 1e-3_f64;
let r_clean = array![0.4_f64, -0.6, 1.1, 0.3, 0.0];
let r_total = &r_clean + &array![0.0_f64, 0.0, 0.0, 0.0, eps_null];
let truth_clean = projected_pseudo_inverse_truth(&h_full, &u_s, &r_clean, &a_k);
let truth_total = projected_pseudo_inverse_truth(&h_full, &u_s, &r_total, &a_k);
let corr_proj_clean = kernel.bilinear_pseudo_inverse(&r_clean, &a_k);
let corr_proj_total = kernel.bilinear_pseudo_inverse(&r_total, &a_k);
assert_relative_eq!(corr_proj_clean, truth_clean, max_relative = 1e-10);
assert_relative_eq!(corr_proj_total, truth_total, max_relative = 1e-10);
assert_relative_eq!(corr_proj_total, corr_proj_clean, max_relative = 1e-12);
assert_relative_eq!(truth_total, truth_clean, max_relative = 1e-12);
let corr_full_clean = dense_h_inv_bilinear_via_eig(&h_full, &r_clean, &a_k);
let corr_full_total = dense_h_inv_bilinear_via_eig(&h_full, &r_total, &a_k);
let full_noise_contrib = corr_full_total - corr_full_clean;
let v_min_dot_a = v_min.dot(&a_k);
let v_min_null = v_min[p - 1];
let predicted_noise = eps_null * v_min_dot_a * v_min_null / small_eig;
assert!(
full_noise_contrib.abs() > 1e6,
"full-H must show 10⁶+ noise amplification; got |Δ| = {:.3e}",
full_noise_contrib.abs()
);
let ratio = full_noise_contrib / predicted_noise;
assert!(
(ratio - 1.0).abs() < 0.05,
"full-H corruption must follow predicted scaling; \
ratio = {ratio:.6}, predicted = {predicted_noise:.3e}, \
actual = {full_noise_contrib:.3e}"
);
let clean_disagreement = corr_full_clean - corr_proj_clean;
assert!(
clean_disagreement.abs() > 1e5,
"fix is self-consistency, NOT denoising: on CLEAN input \
projected ({corr_proj_clean:.3e}) and full-H \
({corr_full_clean:.3e}) must differ by O(1/σ_min); \
got disagreement = {clean_disagreement:.3e}"
);
eprintln!(
"[ift-cross-coupled-airtight] h_proj non-diag ratio = \
{:.3} (max_off / max_diag), clean: projected = \
{corr_proj_clean:.6e}, full = {corr_full_clean:.6e}, \
disagreement = {clean_disagreement:.3e}",
max_offdiag / max_diag
);
eprintln!(
"[ift-cross-coupled-airtight] pollute(ε={eps_null:.0e}): \
projected = {corr_proj_total:.6e}, full = {corr_full_total:.6e}, \
predicted_noise = {predicted_noise:.6e}, actual_noise = \
{full_noise_contrib:.6e}, ratio = {ratio:.6}"
);
}
pub(crate) fn make_factor_key(seed: u64) -> ProjectedFactorKey {
ProjectedFactorKey {
design_id: 1,
factor_ptr: seed as usize,
rows: 1,
cols: 1,
row_stride: 1,
col_stride: 1,
value_hash: seed,
value_hash2: seed.wrapping_mul(31),
}
}
#[test]
pub(crate) fn projected_factor_cache_lru_evicts_oldest_under_budget() {
let entry_floats = 32usize;
let entry_bytes = entry_floats * std::mem::size_of::<f64>();
let cache = ProjectedFactorCache::with_budget(entry_bytes * 2);
let make = |seed: u64| -> Array2<f64> { Array2::from_elem((4, 8), seed as f64) };
let _a = cache.get_or_insert_with(make_factor_key(1), || make(1));
let _b = cache.get_or_insert_with(make_factor_key(2), || make(2));
assert_eq!(cache.len(), 2);
assert_eq!(cache.total_bytes(), entry_bytes * 2);
let _a_again = cache.get_or_insert_with(make_factor_key(1), || make(1));
let _c = cache.get_or_insert_with(make_factor_key(3), || make(3));
assert_eq!(cache.len(), 2);
assert_eq!(cache.total_bytes(), entry_bytes * 2);
let post_a = cache.get_or_insert_with(make_factor_key(1), || make(99));
let post_c = cache.get_or_insert_with(make_factor_key(3), || make(99));
assert_eq!(post_a[[0, 0]], 1.0, "a survived eviction");
assert_eq!(post_c[[0, 0]], 3.0, "c is the freshly inserted entry");
let post_b = cache.get_or_insert_with(make_factor_key(2), || make(99));
assert_eq!(
post_b[[0, 0]],
99.0,
"b was evicted; recompute closure runs"
);
}
#[test]
pub(crate) fn projected_factor_cache_zero_budget_disables_eviction() {
let cache = ProjectedFactorCache::with_budget(0);
for seed in 0..16 {
cache.get_or_insert_with(make_factor_key(seed), || {
Array2::from_elem((8, 8), seed as f64)
});
}
assert_eq!(cache.len(), 16);
}
#[test]
pub(crate) fn projected_factor_cache_oversize_entry_is_cached_unconditionally() {
let cache = ProjectedFactorCache::with_budget(8);
let huge = cache.get_or_insert_with(make_factor_key(1), || Array2::from_elem((4, 4), 1.0));
assert_eq!(huge[[0, 0]], 1.0);
assert_eq!(cache.len(), 1);
}
pub(crate) fn projected_factor_cache_wait_for_subscriber(
cache: &ProjectedFactorCache,
key: ProjectedFactorKey,
timeout: std::time::Duration,
) -> bool {
let marker = {
let inner = cache
.inner
.lock()
.expect("projected factor cache lock poisoned");
let Some(m) = inner.in_progress.get(&key) else {
return false;
};
Arc::clone(m)
};
if marker
.waiter_count
.load(std::sync::atomic::Ordering::Acquire)
> 0
{
return true;
}
let (lock, cv) = &marker.subscriber_arrived;
let mut guard = lock
.lock()
.expect("subscriber-arrived notification lock poisoned");
let deadline = std::time::Instant::now() + timeout;
loop {
if marker
.waiter_count
.load(std::sync::atomic::Ordering::Acquire)
> 0
{
return true;
}
let now = std::time::Instant::now();
if now >= deadline {
return false;
}
let (next_guard, result) = cv
.wait_timeout(guard, deadline - now)
.expect("subscriber-arrived wait poisoned");
guard = next_guard;
if result.timed_out()
&& marker
.waiter_count
.load(std::sync::atomic::Ordering::Acquire)
== 0
{
return false;
}
}
}
#[test]
pub(crate) fn projected_factor_cache_waiters_wake_when_producer_panics() {
let cache = Arc::new(ProjectedFactorCache::with_budget(0));
let key = make_factor_key(42);
let (started_tx, started_rx) = std::sync::mpsc::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel();
let producer_cache = Arc::clone(&cache);
let producer = std::thread::spawn(move || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
producer_cache.get_or_insert_with(key, || {
started_tx.send(()).expect("send producer-start signal");
release_rx.recv().expect("waiter subscribe signal");
panic!("simulated projected-factor panic");
});
}))
.is_err()
});
started_rx
.recv_timeout(std::time::Duration::from_secs(2))
.expect("producer started computing");
let waiter_cache = Arc::clone(&cache);
let waiter = std::thread::spawn(move || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
waiter_cache.get_or_insert_with(key, || Array2::from_elem((1, 1), 7.0));
}))
.is_err()
});
assert!(
projected_factor_cache_wait_for_subscriber(&cache, key, std::time::Duration::from_secs(5),),
"waiter never subscribed to the in-progress slot"
);
release_tx.send(()).expect("release producer");
assert!(producer.join().expect("producer thread joined"));
assert!(waiter.join().expect("waiter thread joined"));
let recovered = cache.get_or_insert_with(key, || Array2::from_elem((1, 1), 9.0));
assert_eq!(recovered[[0, 0]], 9.0);
}
pub(crate) struct SentinelOuterHessianOperator {
pub(crate) matrix: Array2<f64>,
}
impl crate::solver::outer_strategy::OuterHessianOperator for SentinelOuterHessianOperator {
fn dim(&self) -> usize {
self.matrix.nrows()
}
fn matvec(&self, v: &Array1<f64>) -> Result<Array1<f64>, String> {
Ok(self.matrix.dot(v))
}
fn is_cheap_to_materialize(&self) -> bool {
true
}
}
pub(crate) struct FamilyOperatorOnlyDerivatives {
pub(crate) op: Arc<dyn crate::solver::outer_strategy::OuterHessianOperator>,
}
impl HessianDerivativeProvider for FamilyOperatorOnlyDerivatives {
fn hessian_derivative_correction(
&self,
arr: &Array1<f64>,
) -> Result<Option<Array2<f64>>, String> {
assert!(arr.iter().all(|v| !v.is_nan()));
Ok(None)
}
fn has_corrections(&self) -> bool {
false
}
fn outer_hessian_derivative_kernel(&self) -> Option<OuterHessianDerivativeKernel> {
None
}
fn family_outer_hessian_operator(
&self,
) -> Option<Arc<dyn crate::solver::outer_strategy::OuterHessianOperator>> {
Some(Arc::clone(&self.op))
}
}
pub(crate) fn build_sentinel_tripwire_solution(
dispersion: DispersionHandling,
kkt_residual: Option<ProjectedKktResidual>,
) -> InnerSolution<'static> {
let hop = Arc::new(DenseSpectralOperator::from_symmetric(&Array2::eye(2)).unwrap());
let family_operator = Arc::new(SentinelOuterHessianOperator {
matrix: array![[42.0]],
});
let deriv_provider = FamilyOperatorOnlyDerivatives {
op: family_operator,
};
InnerSolution {
log_likelihood: -1.25,
penalty_quadratic: 0.4,
hessian_op: hop,
beta: array![0.5, -0.25],
penalty_coords: vec![PenaltyCoordinate::from_dense_root(Array2::eye(2))],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![1.0e20],
second: Some(array![[0.0]]),
},
deriv_provider: Box::new(deriv_provider),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 2,
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion,
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
}
}
pub(crate) fn dense_and_materialized_outer_hessian(
solution: &InnerSolution<'_>,
rho: &[f64],
lambdas: &[f64],
) -> (Array2<f64>, UnifiedOuterHessianOperator, Array2<f64>) {
let dense = compute_outer_hessian(
solution,
rho,
lambdas,
solution.hessian_op.as_ref(),
solution.deriv_provider.as_ref(),
None,
)
.unwrap();
let kernel = solution
.deriv_provider
.outer_hessian_derivative_kernel()
.unwrap();
let operator = build_outer_hessian_operator(
solution,
lambdas,
solution.deriv_provider.as_ref(),
kernel,
None,
None,
)
.unwrap();
let materialized =
crate::solver::outer_strategy::OuterHessianOperator::materialize_dense(&operator).unwrap();
(dense, operator, materialized)
}
#[test]
pub(crate) fn value_gradient_hessian_prefers_family_supplied_outer_operator() {
let hop = Arc::new(DenseSpectralOperator::from_symmetric(&Array2::eye(2)).unwrap());
let family_matrix = array![[42.0]];
let family_operator = Arc::new(SentinelOuterHessianOperator {
matrix: family_matrix.clone(),
});
let deriv_provider = FamilyOperatorOnlyDerivatives {
op: family_operator,
};
let solution = InnerSolution {
log_likelihood: -1.25,
penalty_quadratic: 0.4,
hessian_op: hop,
beta: array![0.5, -0.25],
penalty_coords: vec![PenaltyCoordinate::from_dense_root(Array2::eye(2))],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![0.0],
second: Some(array![[0.0]]),
},
deriv_provider: Box::new(deriv_provider),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 2,
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::ProfiledGaussian,
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
};
let result = reml_laml_evaluate(&solution, &[0.0], EvalMode::ValueGradientHessian, None)
.expect("family outer operator evaluation");
let crate::solver::outer_strategy::HessianResult::Operator(op) = result.hessian else {
panic!("expected family-supplied operator Hessian route");
};
let dense = op.materialize_dense().expect("sentinel materialization");
assert_eq!(dense, family_matrix);
}
#[test]
pub(crate) fn ift_gradient_correction_with_zero_projected_residual_is_zero() {
let h = Array2::eye(3);
let hop = DenseSpectralOperator::from_symmetric(&h).unwrap();
let solution = build_gaussian_solution_at_beta(&[0.0, 0.0], array![0.5, -0.25, 0.1], false);
let lambdas = [1.0_f64, 1.0_f64];
let penalty_a_k_betas = vec![array![0.3, -0.7, 0.0], array![0.0, 0.0, 0.5]];
let zero_residual = Array1::<f64>::zeros(hop.dim());
let corrections = compute_kkt_residual_rho_corrections(
&solution,
&hop,
&lambdas,
&penalty_a_k_betas,
&zero_residual,
true,
&[false, false],
)
.expect("kkt correction must succeed at zero residual");
for (i, &g) in corrections.gradient.iter().enumerate() {
assert_eq!(
g, 0.0,
"BUG-1: IFT gradient correction at coord {} must be exactly 0.0 when \
r_proj = 0 (q = H⁻¹·0 = 0); got {:.3e}. The cert path's projected residual \
≈ 0 contract therefore gives ZERO correction to the envelope gradient — \
inflated ½ tr(H⁻¹·∂H/∂ρ) is left uncancelled.",
i, g
);
}
let h_corr = corrections.hessian.expect("hessian requested");
for ((i, j), &v) in h_corr.indexed_iter() {
assert_eq!(
v, 0.0,
"BUG-1 hessian: entry ({}, {}) must be 0; got {:.3e}",
i, j, v
);
}
}
#[test]
pub(crate) fn ift_rho_upper_bound_masks_residual_correction_direction() {
let h = Array2::eye(3);
let hop = DenseSpectralOperator::from_symmetric(&h).unwrap();
let solution = build_gaussian_solution_at_beta(&[0.0, 0.0], array![0.5, -0.25, 0.1], false);
let lambdas = [1.0_f64, 1.0_f64];
let penalty_a_k_betas = vec![array![0.3, -0.7, 0.0], array![0.0, 0.0, 0.5]];
let residual = array![0.2, -0.3, 0.4];
let corrections = compute_kkt_residual_rho_corrections(
&solution,
&hop,
&lambdas,
&penalty_a_k_betas,
&residual,
true,
&[false, true],
)
.expect("kkt correction must succeed with a masked upper-bound coordinate");
assert_eq!(
corrections.gradient[1], 0.0,
"upper-bound rho direction must not receive IFT residual gradient correction"
);
let hessian = corrections.hessian.expect("hessian requested");
for idx in 0..2 {
assert_eq!(
hessian[[1, idx]],
0.0,
"upper-bound rho row must be zeroed in IFT residual Hessian correction"
);
assert_eq!(
hessian[[idx, 1]],
0.0,
"upper-bound rho column must be zeroed in IFT residual Hessian correction"
);
}
}
#[test]
pub(crate) fn cert_zero_residual_must_not_emit_unbounded_gradient_through_gate() {
let solution = build_sentinel_tripwire_solution(
DispersionHandling::ProfiledGaussian,
Some(ProjectedKktResidual::from_active_projected(array![
0.0, 0.0
])),
);
let result = reml_laml_evaluate(&solution, &[0.0], EvalMode::ValueGradientHessian, None)
.expect("constrained-stationary cert evaluation");
let cost_scale = result.cost.abs().max(1.0);
let resolve_step = f64::EPSILON.sqrt();
let max_abs = match result.gradient.as_ref() {
Some(g) => g.iter().map(|v| v.abs()).fold(0.0_f64, f64::max),
None => 0.0, };
let predicted_change = max_abs * resolve_step;
let ratio = predicted_change / cost_scale;
assert!(
result.gradient.is_none() || (ratio <= 4.0 && max_abs.is_finite()),
"BUG-2 REGRESSION: cert exit with r_proj = 0 passed an inflated gradient \
through the tripwire. |grad|∞ = {:.3e}, predicted Δcost along √ε step = \
{:.3e}, cost = {:.3e}, ratio = {:.3e} (must be ≤ 4 OR gradient = None). \
At r_proj = 0 the projected-residual correction is identically zero \
(see BUG-1 test), so the inflated envelope gradient must remain \
unavailable to the outer optimizer.",
max_abs,
predicted_change,
cost_scale,
ratio,
);
}
#[test]
pub(crate) fn theta_mode_response_kernel_matches_preport_assembly_bitwise() {
use crate::solver::estimate::reml::unified::ActiveLinearConstraintBlock;
let h = array![[2.0, 0.3, 0.1], [0.3, 1.5, 0.2], [0.1, 0.2, 1.0]];
let hop = DenseSpectralOperator::from_symmetric(&h).unwrap();
let rhs_a = array![0.7, -1.3, 0.4];
let rhs_b = array![-0.2, 0.9, 2.1];
let kernel_free = ThetaModeResponseKernel::select(None, None, &hop);
let one_new = kernel_free.respond_one(&rhs_a);
let one_ref = hop.solve(&rhs_a);
for (n, r) in one_new.iter().zip(one_ref.iter()) {
assert_eq!(n.to_bits(), r.to_bits(), "respond_one vs hop.solve");
}
let mut stack = Array2::<f64>::zeros((3, 3));
stack.column_mut(0).assign(&rhs_a);
stack.column_mut(2).assign(&rhs_b);
let stack_new = kernel_free.respond_stack(&stack);
let stack_ref = hop.solve_multi(&stack);
for (n, r) in stack_new.iter().zip(stack_ref.iter()) {
assert_eq!(n.to_bits(), r.to_bits(), "respond_stack vs solve_multi");
}
assert!(
stack_new.column(1).iter().all(|v| *v == 0.0),
"masked zero RHS column must emit exact zeros"
);
let trace = PenaltySubspaceTrace {
u_s: array![[1.0, 0.0], [0.0, 1.0], [0.0, 0.0]],
h_proj_inverse: array![[0.5, 0.1], [0.1, 0.8]],
};
let block = ActiveLinearConstraintBlock {
a: array![[1.0, 1.0, 0.0]],
};
let kernel_con = ThetaModeResponseKernel::select(Some(&trace), Some(&block), &hop);
let ck_ref = trace.with_active_constraints(block.a.view());
assert!(ck_ref.has_active_constraints());
for rhs in [&rhs_a, &rhs_b] {
let v_new = kernel_con.respond_one(rhs);
let v_ref = ck_ref.apply_pseudo_inverse(rhs);
for (n, r) in v_new.iter().zip(v_ref.iter()) {
assert_eq!(n.to_bits(), r.to_bits(), "constrained respond_one vs K_T");
}
let tangency = block.a.dot(&v_new);
assert!(
tangency[0].abs() <= 1e-12 * (1.0 + v_new.iter().map(|x| x.abs()).sum::<f64>()),
"constrained mode response must satisfy A_act·v = 0, got {}",
tangency[0]
);
}
let con_stack_new = kernel_con.respond_stack(&stack);
for (j, rhs) in [Some(&rhs_a), None, Some(&rhs_b)].iter().enumerate() {
let v_ref = match rhs {
Some(rhs) => ck_ref.apply_pseudo_inverse(rhs),
None => Array1::<f64>::zeros(3),
};
for (n, r) in con_stack_new.column(j).iter().zip(v_ref.iter()) {
assert_eq!(
n.to_bits(),
r.to_bits(),
"constrained respond_stack column {j} vs per-vector K_T"
);
}
}
let kernel_subspace_only = ThetaModeResponseKernel::select(Some(&trace), None, &hop);
let v_sub = kernel_subspace_only.respond_one(&rhs_a);
for (n, r) in v_sub.iter().zip(one_ref.iter()) {
assert_eq!(
n.to_bits(),
r.to_bits(),
"subspace-without-constraints must route through the full H⁻¹"
);
}
}
#[test]
pub(crate) fn envelope_gradient_uses_constraint_tangent_projection() {
use crate::solver::estimate::reml::unified::ActiveLinearConstraintBlock;
let xtx = array![[2.0, 0.1, 0.0], [0.1, 2.0, 0.0], [0.0, 0.0, 1.0e-10]];
let s1 = array![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]];
let s2 = array![[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
let xty = array![1.0, 0.5, 0.0];
let rho = vec![0.0_f64, 0.0_f64];
let lambdas: Vec<f64> = rho.iter().map(|r| r.exp()).collect();
let mut h_mat = xtx.clone();
h_mat.scaled_add(lambdas[0], &s1);
h_mat.scaled_add(lambdas[1], &s2);
let op = DenseSpectralOperator::from_symmetric(&h_mat).unwrap();
let beta_unconstrained = op.solve(&xty);
assert!(beta_unconstrained[2] < 0.5);
let beta_hat = array![xty[0] / h_mat[[0, 0]], xty[1] / h_mat[[1, 1]], 0.5];
let a_act = array![[0.0, 0.0, 1.0]];
let active = ActiveLinearConstraintBlock { a: a_act };
let mut sol = build_gaussian_solution_at_beta(&rho, beta_hat.clone(), true);
sol.dispersion = DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
};
sol.active_constraints = Some(Arc::new(active));
let result = reml_laml_evaluate(&sol, &rho, EvalMode::ValueAndGradient, None);
match result {
Ok(r) => {
let grad = r
.gradient
.expect("tangent dispatch must emit a finite projected gradient");
let max_abs = grad.iter().map(|g| g.abs()).fold(0.0_f64, f64::max);
assert!(
max_abs.is_finite() && max_abs < 1.0e6,
"projected gradient must be bounded by σ_min(ZᵀHZ)⁻¹ (O(1) here); \
got |grad|∞ = {:.3e}",
max_abs,
);
}
Err(err) => {
panic!("tangent-projected evaluator must succeed under active constraints, got: {err}")
}
}
}
#[test]
pub(crate) fn aou_missing_projected_kkt_residual_is_contract_error() {
let solution = build_sentinel_tripwire_solution(
DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
},
None,
);
let err = match reml_laml_evaluate(&solution, &[0.0], EvalMode::ValueGradientHessian, None) {
Ok(_) => panic!("missing projected KKT residual must be a hard contract error"),
Err(err) => err,
};
assert!(
err.contains("fixed-dispersion derivative contract violated")
&& err.contains("BlockwiseInnerResult::kkt_residual"),
"unexpected error for missing projected residual: {err}"
);
}
#[test]
pub(crate) fn envelope_inconsistent_gradient_skips_outer_hessian_assembly() {
let solution = build_sentinel_tripwire_solution(DispersionHandling::ProfiledGaussian, None);
let result = reml_laml_evaluate(&solution, &[0.0], EvalMode::ValueGradientHessian, None)
.expect("envelope tripwire evaluation");
assert!(
result.gradient.is_none(),
"inconsistent envelope gradient should be suppressed"
);
assert!(
matches!(
result.hessian,
crate::solver::outer_strategy::HessianResult::Unavailable
),
"inconsistent envelope gradient should skip Hessian assembly"
);
}
#[test]
pub(crate) fn test_dense_spectral_operator_simple() {
let h = Array2::from_diag(&array![2.0, 5.0]);
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let expected_logdet = 2.0_f64.ln() + 5.0_f64.ln();
assert!((op.logdet() - expected_logdet).abs() < 1e-12);
let id = Array2::eye(2);
let trace = op.trace_hinv_product(&id);
assert!((trace - 0.7).abs() < 1e-12);
let rhs = array![1.0, 1.0];
let sol = op.solve(&rhs);
assert!((sol[0] - 0.5).abs() < 1e-12);
assert!((sol[1] - 0.2).abs() < 1e-12);
assert_eq!(sol.len(), 2);
}
#[test]
pub(crate) fn test_dense_spectral_operator_solve_multi_matches_column_solves() {
let h = array![[4.0, 1.0, 0.5], [1.0, 3.0, 0.25], [0.5, 0.25, 2.0],];
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let rhs = array![[1.0, -1.0], [0.5, 2.0], [3.0, 0.25],];
let multi = op.solve_multi(&rhs);
for col in 0..rhs.ncols() {
let single = op.solve(&rhs.column(col).to_owned());
for row in 0..rhs.nrows() {
let err = (multi[[row, col]] - single[row]).abs();
assert!(
err < 1e-12,
"solve_multi mismatch at ({row}, {col}): multi={}, single={}",
multi[[row, col]],
single[row]
);
}
}
}
#[test]
pub(crate) fn test_dense_spectral_operator_cross_trace_matches_column_solves() {
assert!(file!().ends_with(".rs"));
let h = array![[4.0, 1.0, 0.5], [1.0, 3.0, 0.25], [0.5, 0.25, 2.0],];
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let a = array![[1.0, 0.2, -0.1], [0.2, 2.0, 0.3], [-0.1, 0.3, 0.5],];
let b = array![[0.5, -0.4, 0.1], [-0.4, 1.5, 0.25], [0.1, 0.25, 0.75],];
let expected = (&op.solve_multi(&a).t() * &op.solve_multi(&b)).sum();
let exact = op.trace_hinv_product_cross(&a, &b);
assert_relative_eq!(exact, expected, epsilon = 1e-12, max_relative = 1e-12);
}
#[test]
pub(crate) fn test_dense_spectral_operator_operator_cross_matches_dense_formula() {
assert!(file!().ends_with(".rs"));
let h = array![[5.0, 0.5, 0.25], [0.5, 3.5, 0.2], [0.25, 0.2, 2.5],];
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let dense = array![[1.0, 0.1, -0.2], [0.1, 0.75, 0.3], [-0.2, 0.3, 1.25],];
let other = array![[0.6, -0.3, 0.15], [-0.3, 1.1, 0.05], [0.15, 0.05, 0.9],];
let other_op = DenseMatrixHyperOperator {
matrix: other.clone(),
};
let expected = op.trace_hinv_product_cross(&dense, &other);
let mixed = op.trace_hinv_matrix_operator_cross(&dense, &other_op);
let operator = op.trace_hinv_operator_cross(&other_op, &other_op);
let operator_expected = op.trace_hinv_product_cross(&other, &other);
assert_relative_eq!(mixed, expected, epsilon = 1e-12, max_relative = 1e-12);
assert_relative_eq!(
operator,
operator_expected,
epsilon = 1e-12,
max_relative = 1e-12
);
}
#[test]
pub(crate) fn test_hyper_coord_total_drift_result_keeps_operator_and_dense_correction() {
assert!(file!().ends_with(".rs"));
let h = array![[4.0, 0.25], [0.25, 3.0],];
let hop = DenseSpectralOperator::from_symmetric(&h).unwrap();
let base = array![[1.0, 0.2], [0.2, 0.5],];
let corr = array![[0.3, -0.1], [-0.1, 0.4],];
let drift = HyperCoordDrift::from_operator(Arc::new(DenseMatrixHyperOperator {
matrix: base.clone(),
}));
let correction = DriftDerivResult::Dense(corr.clone());
let combined = hyper_coord_total_drift_result(&drift, Some(&correction), h.nrows());
let expected = hop.trace_logdet_gradient(&(&base + &corr));
assert_relative_eq!(
combined.trace_logdet(&hop),
expected,
epsilon = 1e-12,
max_relative = 1e-12
);
}
#[test]
pub(crate) fn test_dense_spectral_operator_rotated_logdet_cross_matches_dense_path() {
assert!(file!().ends_with(".rs"));
let h = array![[4.0, 0.5, 0.2], [0.5, 2.5, 0.3], [0.2, 0.3, 1.75],];
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let a = array![[0.8, 0.2, -0.1], [0.2, 1.4, 0.35], [-0.1, 0.35, 0.9],];
let b = array![[1.2, -0.25, 0.05], [-0.25, 0.7, 0.15], [0.05, 0.15, 0.6],];
let a_rot = op.rotate_to_eigenbasis(&a);
let b_rot = op.rotate_to_eigenbasis(&b);
let direct = op.trace_logdet_hessian_cross(&a, &b);
let rotated = op.trace_logdet_hessian_cross_rotated(&a_rot, &b_rot);
assert_relative_eq!(rotated, direct, epsilon = 1e-12, max_relative = 1e-12);
}
#[test]
pub(crate) fn test_compute_adjoint_z_c_streaming_matches_dense_reference() {
assert!(file!().ends_with(".rs"));
let n = 64usize;
let p = 8usize;
let mut rng = Xoshiro256SS::from_seed(0x5EED_C0FFEE_u64);
let unit = |rng: &mut Xoshiro256SS| {
let bits = rng.next_u64() >> 11;
(bits as f64) / ((1u64 << 53) as f64) * 2.0 - 1.0
};
let mut x_data = Array2::<f64>::zeros((n, p));
for i in 0..n {
for j in 0..p {
x_data[[i, j]] = unit(&mut rng);
}
}
let mut c_array = Array1::<f64>::zeros(n);
for i in 0..n {
c_array[i] = unit(&mut rng);
}
let mut m = Array2::<f64>::zeros((p, p));
for i in 0..p {
for j in 0..p {
m[[i, j]] = unit(&mut rng);
}
}
let mut h = m.t().dot(&m);
for i in 0..p {
h[[i, i]] += p as f64;
}
let hop = DenseSpectralOperator::from_symmetric(&h).unwrap();
let x = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x_data.clone()));
let ing = ScalarGlmIngredients {
c_array: &c_array,
d_array: None,
x: &x,
};
let z_full = hop.solve_multi(&x_data.t().to_owned());
let mut h_dense = Array1::<f64>::zeros(n);
for i in 0..n {
let mut acc = 0.0;
for j in 0..p {
acc += x_data[[i, j]] * z_full[[j, i]];
}
h_dense[i] = acc;
}
let streamed = compute_adjoint_z_c(&ing, &hop, &h_dense, None).expect("adjoint path");
let mut t = h_dense.clone();
Zip::from(&mut t)
.and(&c_array)
.for_each(|t_i, &c_i| *t_i *= c_i);
let v = x_data.t().dot(&t);
let reference = hop.solve(&v);
for k in 0..p {
assert_relative_eq!(
streamed[k],
reference[k],
epsilon = 1e-12,
max_relative = 1e-12
);
}
}
#[test]
pub(crate) fn fourth_derivative_trace_matrix_matches_scalar_pair_formula() {
assert!(file!().ends_with(".rs"));
let n = 37usize;
let p = 5usize;
let t = 4usize;
let mut rng = Xoshiro256SS::from_seed(0xF047_ACE5_u64);
let unit = |rng: &mut Xoshiro256SS| {
let bits = rng.next_u64() >> 11;
(bits as f64) / ((1u64 << 53) as f64) * 2.0 - 1.0
};
let mut x_data = Array2::<f64>::zeros((n, p));
for i in 0..n {
for j in 0..p {
x_data[[i, j]] = unit(&mut rng);
}
}
let mut c_array = Array1::<f64>::zeros(n);
let mut d_array = Array1::<f64>::zeros(n);
let mut leverage = Array1::<f64>::zeros(n);
for i in 0..n {
c_array[i] = unit(&mut rng);
d_array[i] = unit(&mut rng);
leverage[i] = 0.25 + unit(&mut rng).abs();
}
let x = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x_data));
let ing = ScalarGlmIngredients {
c_array: &c_array,
d_array: Some(&d_array),
x: &x,
};
let mut modes = Vec::with_capacity(t);
for _ in 0..t {
let mut mode = Array1::<f64>::zeros(p);
for j in 0..p {
mode[j] = unit(&mut rng);
}
modes.push(mode);
}
let mode_refs = modes.iter().collect::<Vec<_>>();
let gram = compute_fourth_derivative_trace_matrix(&ing, &mode_refs, &leverage)
.expect("batched fourth trace")
.expect("d-array is present");
for i in 0..t {
for j in 0..t {
let scalar = compute_fourth_derivative_trace(&ing, &modes[i], &modes[j], &leverage)
.expect("scalar fourth trace")
.expect("d-array is present");
assert_relative_eq!(gram[[i, j]], scalar, epsilon = 1e-10, max_relative = 1e-10);
}
}
}
#[test]
pub(crate) fn operator_hessian_matches_dense_with_operator_drifts_and_extended_glm_corrections() {
let h = array![[1.0e-7, 0.0], [0.0, 2.7]];
let hop = Arc::new(DenseSpectralOperator::from_symmetric(&h).unwrap());
let beta = array![0.4, -0.7];
let penalty_root = array![[1.2, 0.1], [0.0, 0.8]];
let ext_drift = array![[0.45, -0.15], [-0.15, 0.35]];
let x = array![[1.0, 0.2], [-0.4, 1.1], [0.7, -0.8]];
let c_array = array![0.31, -0.27, 0.19];
let d_array = array![0.17, -0.11, 0.23];
let deriv_provider = SinglePredictorGlmDerivatives {
c_array,
d_array: Some(d_array),
hessian_weights: Array1::ones(3),
x_transformed: DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x)),
};
let solution = InnerSolution {
log_likelihood: -2.3,
penalty_quadratic: 0.6,
hessian_op: hop.clone(),
beta,
penalty_coords: vec![PenaltyCoordinate::from_dense_root(penalty_root)],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![0.4],
second: Some(array![[0.13]]),
},
deriv_provider: Box::new(deriv_provider),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 3,
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
},
ext_coords: vec![HyperCoord {
a: -0.21,
g: array![0.33, -0.42],
drift: HyperCoordDrift::from_operator(Arc::new(DenseMatrixHyperOperator {
matrix: ext_drift,
})),
ld_s: 0.07,
b_depends_on_beta: false,
is_penalty_like: false,
firth_g: None,
tk_eta_fixed: None,
tk_x_fixed: None,
}],
ext_coord_pair_fn: Some(Box::new(|_, _| HyperCoordPair {
a: 0.09,
g: array![0.16, -0.12],
b_mat: array![[0.08, 0.03], [0.03, -0.04]],
b_operator: None,
ld_s: -0.05,
})),
rho_ext_pair_fn: Some(Box::new(|_, _| HyperCoordPair {
a: -0.14,
g: array![-0.18, 0.22],
b_mat: array![[0.05, -0.02], [-0.02, 0.07]],
b_operator: None,
ld_s: 0.04,
})),
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
};
let rho: Vec<f64> = vec![0.2_f64];
let lambdas: Vec<f64> = rho.iter().map(|value| value.exp()).collect();
let (dense, operator, materialized) =
dense_and_materialized_outer_hessian(&solution, &rho, &lambdas);
for row in 0..dense.nrows() {
for col in 0..dense.ncols() {
let materialized_entry = materialized[[row, col]];
let dense_entry = dense[[row, col]];
let tolerance = 1e-10_f64.max(1e-10 * dense_entry.abs());
assert!(
(materialized_entry - dense_entry).abs() <= tolerance,
"outer Hessian operator mismatch at ({row}, {col}): materialized={materialized_entry}, dense={dense_entry}"
);
}
}
let alpha = array![0.37, -0.58];
let hvp = crate::solver::outer_strategy::OuterHessianOperator::matvec(&operator, &alpha)
.expect("operator HVP");
let dense_hvp = dense.dot(&alpha);
for i in 0..hvp.len() {
let tolerance = 1e-10_f64.max(1e-10 * dense_hvp[i].abs());
assert!(
(hvp[i] - dense_hvp[i]).abs() <= tolerance,
"outer Hessian HVP mismatch at {i}: operator={}, dense={}",
hvp[i],
dense_hvp[i]
);
}
}
#[test]
pub(crate) fn operator_hessian_with_contracted_psi_hook_matches_per_pair_dense() {
let h = array![[1.3, 0.2], [0.2, 2.1]];
let hop = Arc::new(DenseSpectralOperator::from_symmetric(&h).unwrap());
let beta = array![0.4, -0.7];
let penalty_root = array![[1.2, 0.1], [0.0, 0.8]];
let ext_drift = array![[0.45, -0.15], [-0.15, 0.35]];
let x = array![[1.0, 0.2], [-0.4, 1.1], [0.7, -0.8]];
let c_array = array![0.31, -0.27, 0.19];
let d_array = array![0.17, -0.11, 0.23];
let psi_pair_a = 0.09_f64;
let psi_pair_g = array![0.16_f64, -0.12];
let psi_pair_b = array![[0.85_f64, 0.30], [0.30, 0.62]];
let psi_pair_ld_s = -0.05_f64;
let build_solution = |with_hook: bool| {
let deriv_provider = SinglePredictorGlmDerivatives {
c_array: c_array.clone(),
d_array: Some(d_array.clone()),
hessian_weights: Array1::ones(3),
x_transformed: DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x.clone())),
};
let contracted_psi_second_order: Option<ContractedPsiSecondOrderFn> = if with_hook {
let g = psi_pair_g.clone();
let b = psi_pair_b.clone();
Some(Arc::new(move |alpha_psi: &[f64]| {
let a0 = alpha_psi[0];
Ok(Some(ContractedPsiSecondOrder {
objective: array![a0 * psi_pair_a],
score: {
let mut s = Array2::<f64>::zeros((1, 2));
s.row_mut(0).assign(&g.mapv(|v| a0 * v));
s
},
hessian: vec![DriftDerivResult::Dense(b.mapv(|v| a0 * v))],
ld_s: array![a0 * psi_pair_ld_s],
}))
}) as ContractedPsiSecondOrderFn)
} else {
None
};
let pair_b_for_dense = psi_pair_b.clone();
InnerSolution {
log_likelihood: -2.3,
penalty_quadratic: 0.6,
hessian_op: hop.clone(),
beta: beta.clone(),
penalty_coords: vec![PenaltyCoordinate::from_dense_root(penalty_root.clone())],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![0.4],
second: Some(array![[0.13]]),
},
deriv_provider: Box::new(deriv_provider),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 3,
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
},
ext_coords: vec![HyperCoord {
a: -0.21,
g: array![0.33, -0.42],
drift: HyperCoordDrift::from_operator(Arc::new(DenseMatrixHyperOperator {
matrix: ext_drift.clone(),
})),
ld_s: 0.07,
b_depends_on_beta: false,
is_penalty_like: false,
firth_g: None,
tk_eta_fixed: None,
tk_x_fixed: None,
}],
ext_coord_pair_fn: Some(Box::new(move |_, _| HyperCoordPair {
a: psi_pair_a,
g: array![0.16, -0.12],
b_mat: pair_b_for_dense.clone(),
b_operator: None,
ld_s: psi_pair_ld_s,
})),
rho_ext_pair_fn: Some(Box::new(|_, _| HyperCoordPair {
a: -0.14,
g: array![-0.18, 0.22],
b_mat: array![[0.05, -0.02], [-0.02, 0.07]],
b_operator: None,
ld_s: 0.04,
})),
fixed_drift_deriv: None,
contracted_psi_second_order,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
}
};
let rho: Vec<f64> = vec![0.2_f64];
let lambdas: Vec<f64> = rho.iter().map(|value| value.exp()).collect();
let dense_solution = build_solution(false);
let dense = compute_outer_hessian(
&dense_solution,
&rho,
&lambdas,
dense_solution.hessian_op.as_ref(),
dense_solution.deriv_provider.as_ref(),
None,
)
.unwrap();
let hook_solution = build_solution(true);
let kernel = hook_solution
.deriv_provider
.outer_hessian_derivative_kernel()
.unwrap();
let operator = build_outer_hessian_operator(
&hook_solution,
&lambdas,
hook_solution.deriv_provider.as_ref(),
kernel,
None,
None,
)
.unwrap();
assert!(
matches!(
crate::solver::outer_strategy::OuterHessianOperator::materialization_capability(
&operator
),
crate::solver::outer_strategy::OuterHessianMaterialization::Unavailable
),
"#740 operator must advertise Unavailable materialization to stay matrix-free"
);
let materialized =
crate::solver::outer_strategy::OuterHessianOperator::materialize_dense(&operator).unwrap();
let control_solution = build_solution(false);
let control_kernel = control_solution
.deriv_provider
.outer_hessian_derivative_kernel()
.unwrap();
let control_operator = build_outer_hessian_operator(
&control_solution,
&lambdas,
control_solution.deriv_provider.as_ref(),
control_kernel,
None,
None,
)
.unwrap();
let control_mat =
crate::solver::outer_strategy::OuterHessianOperator::materialize_dense(&control_operator)
.unwrap();
for row in 0..dense.nrows() {
for col in 0..dense.ncols() {
let c = control_mat[[row, col]];
let d = dense[[row, col]];
let tol = 1e-9_f64.max(1e-9 * d.abs());
assert!(
(c - d).abs() <= tol,
"#740 CONTROL (no-hook operator) mismatch at ({row}, {col}): no-hook-op={c}, \
per-pair-dense={d} — pre-existing operator path differs from dense, so the \
#740 hook comparison below is not the right oracle; investigate the table fill"
);
}
}
for row in 0..dense.nrows() {
for col in 0..dense.ncols() {
let m = materialized[[row, col]];
let d = dense[[row, col]];
let c = control_mat[[row, col]];
let tol = 1e-9_f64.max(1e-9 * d.abs());
assert!(
(m - d).abs() <= tol,
"#740 contracted-hook operator mismatch at ({row}, {col}): \
hook-operator={m}, per-pair-dense={d}, no-hook-control={c} \
(control==dense ⇒ bug is in the #740 hook injection at this entry)"
);
}
}
let psi_diag_full = dense[[1, 1]];
let dense_no_base = {
let mut sol = build_solution(false);
sol.ext_coord_pair_fn = Some(Box::new(move |_, _| HyperCoordPair {
a: psi_pair_a,
g: array![0.16, -0.12],
b_mat: Array2::zeros((2, 2)),
b_operator: None,
ld_s: psi_pair_ld_s,
}));
compute_outer_hessian(
&sol,
&rho,
&lambdas,
sol.hessian_op.as_ref(),
sol.deriv_provider.as_ref(),
None,
)
.unwrap()[[1, 1]]
};
let dense_no_term2 = {
let mut sol = build_solution(false);
sol.deriv_provider = Box::new(SinglePredictorGlmDerivatives {
c_array: Array1::zeros(3),
d_array: Some(Array1::zeros(3)),
hessian_weights: Array1::ones(3),
x_transformed: DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x.clone())),
});
compute_outer_hessian(
&sol,
&rho,
&lambdas,
sol.hessian_op.as_ref(),
sol.deriv_provider.as_ref(),
None,
)
.unwrap()[[1, 1]]
};
assert!(
(psi_diag_full - dense_no_base).abs() > 1e-6,
"#740 test is vacuous: base_h2 ψψ contributes ~0 at the ψ diagonal \
(full={psi_diag_full}, term2-only={dense_no_base}); pick a fixture where it is live"
);
assert!(
(psi_diag_full - dense_no_term2).abs() > 1e-6,
"#740 test is vacuous: the family correction (term2) contributes ~0 at the ψ \
diagonal (full={psi_diag_full}, base_h2-only={dense_no_term2}); pick a fixture \
where it is live"
);
let mixed = array![0.6_f64, -1.1_f64]; let hvp = crate::solver::outer_strategy::OuterHessianOperator::matvec(&operator, &mixed)
.expect("mixed-direction operator HVP");
let dense_hvp = dense.dot(&mixed);
for i in 0..hvp.len() {
let tol = 1e-10_f64.max(1e-10 * dense_hvp[i].abs());
assert!(
(hvp[i] - dense_hvp[i]).abs() <= tol,
"#740 mixed-ρψ HVP mismatch at {i}: hook-operator={}, per-pair-dense={} \
— a ρψ/ψψ block-split error (double-count or dropped entry) in the cross",
hvp[i],
dense_hvp[i]
);
}
}
#[test]
pub(crate) fn subspace_projected_leverage_and_adjoint_shortcut_match_dense() {
let u_s = array![[1.0, 0.0], [0.0, 1.0], [0.0, 0.0], [0.0, 0.0]];
let det = 3.0_f64 * 5.0 - 0.1 * 0.1;
let h_proj_inverse = array![[5.0 / det, -0.1 / det], [-0.1 / det, 3.0 / det]];
let subspace = PenaltySubspaceTrace {
u_s: u_s.clone(),
h_proj_inverse: h_proj_inverse.clone(),
};
let x_data = array![
[1.0, 0.2, 0.5, 0.3],
[1.0, 1.1, -0.2, 0.4],
[1.0, -0.8, 0.7, -0.1],
[1.0, 0.5, 0.3, 0.6]
];
let c = array![0.31_f64, -0.27, 0.19, -0.11];
let k_dense = u_s.dot(&h_proj_inverse).dot(&u_s.t());
let n = x_data.nrows();
let x_design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x_data.clone()));
let h_g_proj = subspace.xt_projected_kernel_x_diagonal(&x_design);
assert_eq!(h_g_proj.len(), n);
for i in 0..n {
let row = x_data.row(i).to_owned();
let kx = k_dense.dot(&row);
assert_relative_eq!(h_g_proj[i], row.dot(&kx), epsilon = 1e-12);
}
let probes = [
array![0.6_f64, -0.4, 0.0, 0.0],
array![0.0_f64, 0.0, 0.5, 0.7],
array![0.3_f64, -0.1, 0.4, -0.2],
array![1.0_f64, 1.0, 1.0, 1.0],
];
for u in probes.iter() {
let xu = x_data.dot(u);
let mut weighted_x = x_data.clone();
for i in 0..n {
let w = c[i] * xu[i];
for j in 0..weighted_x.ncols() {
weighted_x[[i, j]] *= w;
}
}
let c_u_dense = x_data.t().dot(&weighted_x);
let lhs = subspace.trace_projected_logdet(&c_u_dense);
let mut weighted = Array1::<f64>::zeros(n);
for i in 0..n {
weighted[i] = c[i] * h_g_proj[i];
}
let rhs = u.dot(&x_data.t().dot(&weighted));
assert_relative_eq!(lhs, rhs, epsilon = 1e-12, max_relative = 1e-12);
}
}
#[test]
pub(crate) fn subspace_base_h2_traces_match_scalar_projected_kernel_path() {
let h = array![[3.0, 0.1, 0.0], [0.1, 5.0, 0.2], [0.0, 0.2, 7.0]];
let hop = DenseSpectralOperator::from_symmetric(&h).unwrap();
let u_s = array![[1.0, 0.0], [0.0, 1.0], [0.0, 0.0]];
let det = 3.0_f64 * 5.0 - 0.1 * 0.1;
let kernel = PenaltySubspaceTrace {
u_s,
h_proj_inverse: array![[5.0 / det, -0.1 / det], [-0.1 / det, 3.0 / det]],
};
let dense_only = array![[0.4, 0.1, 0.0], [0.1, -0.2, 0.3], [0.0, 0.3, 0.6]];
let op_a = array![[0.2, -0.1, 0.4], [-0.1, 0.7, 0.0], [0.4, 0.0, -0.3]];
let op_b = array![[0.8, 0.2, -0.2], [0.2, 0.1, 0.5], [-0.2, 0.5, 0.9]];
let composite_dense = array![[0.05, 0.02, 0.0], [0.02, 0.03, 0.01], [0.0, 0.01, 0.04]];
let op_a_arc: Arc<dyn HyperOperator> = Arc::new(DenseMatrixHyperOperator {
matrix: op_a.clone(),
});
let op_b_arc: Arc<dyn HyperOperator> = Arc::new(DenseMatrixHyperOperator {
matrix: op_b.clone(),
});
let weighted: Arc<dyn HyperOperator> = Arc::new(WeightedHyperOperator {
terms: vec![(0.25, op_b_arc.clone()), (-0.5, op_a_arc.clone())],
dim_hint: 3,
});
let pairs = [
HyperCoordPair {
a: 0.0,
g: Array1::zeros(3),
b_mat: dense_only,
b_operator: None,
ld_s: 0.0,
},
HyperCoordPair {
a: 0.0,
g: Array1::zeros(3),
b_mat: Array2::zeros((0, 0)),
b_operator: Some(Box::new(DenseMatrixHyperOperator { matrix: op_a })),
ld_s: 0.0,
},
HyperCoordPair {
a: 0.0,
g: Array1::zeros(3),
b_mat: Array2::zeros((0, 0)),
b_operator: Some(Box::new(CompositeHyperOperator {
dense: Some(composite_dense),
operators: vec![weighted, op_b_arc],
dim_hint: 3,
})),
ld_s: 0.0,
},
];
let pair_refs: Vec<&HyperCoordPair> = pairs.iter().collect();
let batched = compute_base_h2_traces(&hop, &pair_refs, Some(&kernel), None);
let scalar: Vec<f64> = pair_refs
.iter()
.map(|pair| {
compute_base_h2_trace(&hop, &pair.b_mat, pair.b_operator.as_deref(), Some(&kernel))
})
.collect();
assert_eq!(batched.len(), scalar.len());
for (idx, (got, expected)) in batched.iter().zip(scalar.iter()).enumerate() {
assert!(
(*got - *expected).abs() <= 1e-12_f64.max(1e-12 * expected.abs()),
"projected base_h2 trace mismatch at pair {idx}: got={got}, expected={expected}"
);
}
}
#[test]
pub(crate) fn outer_hessian_operator_matvec_matches_dense_subspace_with_null_alpha() {
assert!(file!().ends_with(".rs"));
let h = array![
[3.0, 0.1, 0.0, 0.0],
[0.1, 5.0, 0.05, 0.0],
[0.0, 0.05, 7.0, 0.15],
[0.0, 0.0, 0.15, 11.0]
];
let hop = Arc::new(DenseSpectralOperator::from_symmetric(&h).unwrap());
let u_s = array![[1.0, 0.0], [0.0, 1.0], [0.0, 0.0], [0.0, 0.0]];
let det = 3.0_f64 * 5.0 - 0.1 * 0.1;
let h_proj_inverse = array![[5.0 / det, -0.1 / det], [-0.1 / det, 3.0 / det]];
let penalty_root_0 = array![[0.7, 0.3, 0.6, 0.0]];
let penalty_root_1 = array![[0.2, 0.5, 0.0, 0.4]];
let x = array![
[1.0, 0.2, 0.5, 0.3],
[1.0, 1.1, -0.2, 0.4],
[1.0, -0.8, 0.7, -0.1],
[1.0, 0.5, 0.3, 0.6]
];
let c_array = array![0.31, -0.27, 0.19, -0.11];
let d_array = array![0.17, -0.11, 0.23, 0.07];
let deriv_provider = SinglePredictorGlmDerivatives {
c_array,
d_array: Some(d_array),
hessian_weights: Array1::ones(4),
x_transformed: DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x)),
};
let logdet_h_proj = det.ln();
let beta = array![0.4, -0.7, 0.2, 0.1];
let solution = InnerSolution {
log_likelihood: -2.3,
penalty_quadratic: 0.6,
hessian_op: hop.clone(),
beta,
penalty_coords: vec![
PenaltyCoordinate::from_dense_root(penalty_root_0),
PenaltyCoordinate::from_dense_root(penalty_root_1),
],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![0.4, -0.2],
second: Some(array![[0.13, 0.02], [0.02, 0.09]]),
},
deriv_provider: Box::new(deriv_provider),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: logdet_h_proj - hop.logdet(),
penalty_subspace_trace: Some(Arc::new(PenaltySubspaceTrace {
u_s,
h_proj_inverse,
})),
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 4,
nullspace_dim: 2.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
},
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
};
let rho: Vec<f64> = vec![0.2_f64, -0.1];
let lambdas: Vec<f64> = rho.iter().map(|value| value.exp()).collect();
let (dense, operator, materialized) =
dense_and_materialized_outer_hessian(&solution, &rho, &lambdas);
for row in 0..dense.nrows() {
for col in 0..dense.ncols() {
assert_relative_eq!(
materialized[[row, col]],
dense[[row, col]],
epsilon = 1e-12,
max_relative = 1e-12
);
}
}
let alphas = [
array![1.0, 0.0],
array![0.0, 1.0],
array![1.0, 1.0],
array![1.0, -1.0],
array![0.7, -0.3],
];
for alpha in alphas.iter() {
let hvp = crate::solver::outer_strategy::OuterHessianOperator::matvec(&operator, alpha)
.expect("operator HVP");
let dense_hvp = dense.dot(alpha);
for i in 0..hvp.len() {
assert_relative_eq!(hvp[i], dense_hvp[i], epsilon = 1e-12, max_relative = 1e-12);
}
}
}
#[test]
pub(crate) fn projected_operator_hessian_matches_dense_subspace_trace() {
assert!(file!().ends_with(".rs"));
let h = array![[3.0, 0.2], [0.2, 5.0]];
let hop = Arc::new(DenseSpectralOperator::from_symmetric(&h).unwrap());
let beta = array![0.4, -0.7];
let penalty_root = array![[0.0, 1.0]];
let ext_drift = array![[0.45, -0.15], [-0.15, 0.35]];
let x = array![[1.0, 0.2], [1.0, 1.1], [1.0, -0.8], [1.0, 0.5]];
let c_array = array![0.31, -0.27, 0.19, -0.11];
let d_array = array![0.17, -0.11, 0.23, 0.07];
let deriv_provider = SinglePredictorGlmDerivatives {
c_array,
d_array: Some(d_array),
hessian_weights: Array1::ones(4),
x_transformed: DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x)),
};
let h_proj = h[[1, 1]];
let solution = InnerSolution {
log_likelihood: -2.3,
penalty_quadratic: 0.6,
hessian_op: hop.clone(),
beta,
penalty_coords: vec![PenaltyCoordinate::from_dense_root(penalty_root)],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![0.4],
second: Some(array![[0.13]]),
},
deriv_provider: Box::new(deriv_provider),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: h_proj.ln() - hop.logdet(),
penalty_subspace_trace: Some(Arc::new(PenaltySubspaceTrace {
u_s: array![[0.0], [1.0]],
h_proj_inverse: array![[1.0 / h_proj]],
})),
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 4,
nullspace_dim: 1.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
},
ext_coords: vec![HyperCoord {
a: -0.21,
g: array![0.33, -0.42],
drift: HyperCoordDrift::from_operator(Arc::new(DenseMatrixHyperOperator {
matrix: ext_drift,
})),
ld_s: 0.07,
b_depends_on_beta: false,
is_penalty_like: false,
firth_g: None,
tk_eta_fixed: None,
tk_x_fixed: None,
}],
ext_coord_pair_fn: Some(Box::new(|_, _| HyperCoordPair {
a: 0.09,
g: array![0.16, -0.12],
b_mat: array![[0.08, 0.03], [0.03, -0.04]],
b_operator: None,
ld_s: -0.05,
})),
rho_ext_pair_fn: Some(Box::new(|_, _| HyperCoordPair {
a: -0.14,
g: array![-0.18, 0.22],
b_mat: array![[0.05, -0.02], [-0.02, 0.07]],
b_operator: None,
ld_s: 0.04,
})),
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
};
let rho: Vec<f64> = vec![0.2_f64];
let lambdas: Vec<f64> = rho.iter().map(|value| value.exp()).collect();
let (dense, _, materialized) = dense_and_materialized_outer_hessian(&solution, &rho, &lambdas);
for row in 0..dense.nrows() {
for col in 0..dense.ncols() {
assert_relative_eq!(
materialized[[row, col]],
dense[[row, col]],
epsilon = 1e-10,
max_relative = 1e-10
);
}
}
}
#[test]
pub(crate) fn penalty_subspace_batched_reduction_matches_serial_operator_reduction() {
assert!(file!().ends_with(".rs"));
let kernel = PenaltySubspaceTrace {
u_s: array![[1.0, 0.0], [0.2, 0.8], [-0.1, 0.6]],
h_proj_inverse: array![[0.8, 0.1], [0.1, 0.6]],
};
let dense = array![[0.4, 0.1, -0.2], [0.1, 0.7, 0.3], [-0.2, 0.3, 0.5]];
let op_matrix = array![[0.3, -0.2, 0.1], [-0.2, 0.9, 0.4], [0.1, 0.4, 0.8]];
let composite_dense = array![[0.05, 0.01, 0.0], [0.01, -0.02, 0.03], [0.0, 0.03, 0.04]];
let drifts = vec![
DriftDerivResult::Dense(dense.clone()),
DriftDerivResult::Operator(Arc::new(DenseMatrixHyperOperator {
matrix: op_matrix.clone(),
})),
DriftDerivResult::Operator(Arc::new(CompositeHyperOperator {
dim_hint: 3,
dense: Some(composite_dense.clone()),
operators: vec![Arc::new(DenseMatrixHyperOperator {
matrix: op_matrix.clone(),
})],
})),
];
let batched = penalty_subspace_reduce_drifts_batched(&kernel, &drifts);
let serial = [
kernel.reduce(&dense),
kernel.reduce_operator(&DenseMatrixHyperOperator {
matrix: op_matrix.clone(),
}),
kernel.reduce_operator(&CompositeHyperOperator {
dim_hint: 3,
dense: Some(composite_dense),
operators: vec![Arc::new(DenseMatrixHyperOperator { matrix: op_matrix })],
}),
];
for (batched_mat, serial_mat) in batched.iter().zip(serial.iter()) {
for row in 0..batched_mat.nrows() {
for col in 0..batched_mat.ncols() {
assert_relative_eq!(
batched_mat[[row, col]],
serial_mat[[row, col]],
epsilon = 1e-12,
max_relative = 1e-12,
);
}
}
}
}
#[test]
pub(crate) fn subspace_trace_large_k_routes_to_projected_operator() {
let h = array![[3.0, 0.2], [0.2, 5.0]];
let hop = Arc::new(DenseSpectralOperator::from_symmetric(&h).unwrap());
let pcoord = PenaltyCoordinate::from_dense_root(array![[0.0, 1.0]]);
let k = MATRIX_FREE_OUTER_HESSIAN_K_THRESHOLD;
let x = array![[1.0, 0.2], [1.0, 1.1], [1.0, -0.8], [1.0, 0.5]];
let deriv_provider = SinglePredictorGlmDerivatives {
c_array: array![0.31, -0.27, 0.19, -0.11],
d_array: Some(array![0.17, -0.11, 0.23, 0.07]),
hessian_weights: Array1::ones(4),
x_transformed: DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x)),
};
let h_proj = h[[1, 1]];
let solution = InnerSolution {
log_likelihood: -2.3,
penalty_quadratic: 0.6,
hessian_op: hop.clone(),
beta: array![0.4, -0.7],
penalty_coords: vec![pcoord; k],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: Array1::zeros(k),
second: Some(Array2::zeros((k, k))),
},
deriv_provider: Box::new(deriv_provider),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: h_proj.ln() - hop.logdet(),
penalty_subspace_trace: Some(Arc::new(PenaltySubspaceTrace {
u_s: array![[0.0], [1.0]],
h_proj_inverse: array![[1.0 / h_proj]],
})),
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 4,
nullspace_dim: 1.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
},
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
};
let rho = vec![0.0_f64; k];
let result = reml_laml_evaluate(&solution, &rho, EvalMode::ValueGradientHessian, None).unwrap();
assert!(
matches!(
result.hessian,
crate::solver::outer_strategy::HessianResult::Operator(_)
),
"large-k subspace-trace case should use projected outer Hessian operator"
);
}
#[test]
pub(crate) fn test_dense_spectral_operator_singular() {
let h = array![[1.0, 1.0], [1.0, 1.0]];
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let epsilon = spectral_epsilon(&[0.0, 2.0]);
let r0 = spectral_regularize(0.0, epsilon);
let r2 = spectral_regularize(2.0, epsilon);
let expected_logdet = r0.ln() + r2.ln();
assert!((op.logdet() - expected_logdet).abs() < 1e-10);
let trace = op.trace_hinv_product(&Array2::eye(2));
assert!(trace.is_finite());
}
#[test]
pub(crate) fn test_spectral_regularize_stays_finite_in_extreme_tails() {
let epsilon = 1e-8;
let large_negative = spectral_regularize(-1e16, epsilon);
assert!(
large_negative.is_finite() && large_negative > 0.0,
"large negative sigma should regularize to a positive finite value, got {large_negative}"
);
let large_positive = spectral_regularize(1e308, epsilon);
assert!(
large_positive.is_finite() && large_positive > 0.0,
"large positive sigma should stay finite, got {large_positive}"
);
}
#[test]
pub(crate) fn test_smooth_floor_dp() {
let (val, grad, _) = smooth_floor_dp(1.0);
assert!((val - 1.0).abs() < 1e-6);
assert!((grad - 1.0).abs() < 1e-6);
let (val, grad, _) = smooth_floor_dp(DP_FLOOR);
assert!(val > DP_FLOOR);
assert!((grad - 0.5).abs() < 0.1);
let (val, _, _) = smooth_floor_dp(0.0);
assert!(val >= DP_FLOOR);
}
#[test]
pub(crate) fn test_gaussian_derivatives_has_no_corrections() {
let g = GaussianDerivatives;
assert!(!g.has_corrections());
assert!(
g.hessian_derivative_correction(&array![1.0, 2.0])
.unwrap()
.is_none()
);
}
#[test]
pub(crate) fn gaussian_derivatives_advertise_exact_outer_hvp_kernel() {
let g = GaussianDerivatives;
assert!(matches!(
g.outer_hessian_derivative_kernel(),
Some(OuterHessianDerivativeKernel::Gaussian)
));
}
#[test]
pub(crate) fn standard_gam_large_n_gaussian_prefers_operator_when_dense_work_is_large() {
assert!(prefer_outer_hessian_operator(320_000, 42, 6));
assert!(matches!(
GaussianDerivatives.outer_hessian_derivative_kernel(),
Some(OuterHessianDerivativeKernel::Gaussian)
));
}
#[test]
pub(crate) fn callback_outer_hessian_routes_by_row_pair_work_even_at_small_p() {
assert!(!prefer_outer_hessian_operator(155_980, 19, 23));
assert!(outer_hessian_route_plan(155_980, 19, 23, true, true, false).use_operator);
assert!(!outer_hessian_route_plan(155_980, 19, 23, true, false, false).use_operator);
assert!(!outer_hessian_route_plan(1_000, 19, 23, true, true, false).use_operator);
}
#[test]
pub(crate) fn callback_outer_hessian_ignores_generic_large_n_small_p_crossover() {
assert!(prefer_outer_hessian_operator(195_780, 33, 8));
assert!(!outer_hessian_route_plan(195_780, 33, 8, true, true, false).use_operator);
assert!(outer_hessian_route_plan(195_780, 512, 8, true, true, false).use_operator);
assert!(outer_hessian_route_plan(195_780, 33, 32, true, true, false).use_operator);
let plan = outer_hessian_route_plan(195_780, 33, 8, true, true, false);
assert!(!plan.use_operator);
assert_eq!(plan.choice(), "dense");
assert_eq!(plan.reason, "below_crossover");
assert!(!plan.scale_prefers_operator);
}
#[test]
pub(crate) fn outer_hessian_route_respects_dense_workspace_budget() {
let plan = outer_hessian_route_plan(10_000, 10_000, 2, true, true, false);
assert!(plan.use_operator);
assert_eq!(plan.reason, "dense_memory_budget");
assert!(plan.dense_workspace_bytes > outer_hessian_dense_workspace_budget_bytes());
}
#[test]
pub(crate) fn outer_hessian_route_reports_kernel_absent_before_scale_model() {
let plan = outer_hessian_route_plan(1_000_000, 10_000, 64, false, false, false);
assert!(!plan.use_operator);
assert_eq!(plan.reason, "kernel_absent");
assert!(!plan.scale_prefers_operator);
}
#[test]
pub(crate) fn gaussian_outer_hessian_operator_matches_dense_assembly() {
let h = array![[2.4, 0.2], [0.2, 1.7]];
let hop = Arc::new(DenseSpectralOperator::from_symmetric(&h).unwrap());
let beta = array![0.35, -0.55];
let penalty_root_0 = array![[1.0, 0.2], [0.0, 0.4]];
let penalty_root_1 = array![[0.3, -0.1], [0.0, 0.9]];
let solution = InnerSolution {
log_likelihood: -8.0,
penalty_quadratic: 0.9,
hessian_op: hop.clone(),
beta,
penalty_coords: vec![
PenaltyCoordinate::from_dense_root(penalty_root_0),
PenaltyCoordinate::from_dense_root(penalty_root_1),
],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![0.8, 0.6],
second: Some(array![[0.11, 0.03], [0.03, 0.17]]),
},
deriv_provider: Box::new(GaussianDerivatives),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 320_000,
nullspace_dim: 1.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::ProfiledGaussian,
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
};
let rho: Vec<f64> = vec![0.2_f64, -0.4_f64];
let lambdas: Vec<f64> = rho.iter().map(|value| value.exp()).collect();
let (dense, _, materialized) = dense_and_materialized_outer_hessian(&solution, &rho, &lambdas);
for row in 0..dense.nrows() {
for col in 0..dense.ncols() {
let expected = dense[[row, col]];
let actual = materialized[[row, col]];
let tolerance = 1e-10_f64.max(1e-10 * expected.abs());
assert!(
(actual - expected).abs() <= tolerance,
"Gaussian outer Hessian operator mismatch at ({row}, {col}): materialized={actual}, dense={expected}"
);
}
}
}
#[test]
pub(crate) fn efs_step_is_zero_at_scalar_optimum() {
let lambda = 1.0 / 3.0;
let beta_hat = 1.5_f64;
let h = Array2::from_shape_vec((1, 1), vec![1.0 + lambda]).unwrap();
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let penalty_root = Array2::from_shape_vec((1, 1), vec![1.0]).unwrap();
let solution = InnerSolution {
log_likelihood: 0.0,
penalty_quadratic: 0.0,
hessian_op: Arc::new(op),
beta: array![beta_hat],
penalty_coords: vec![PenaltyCoordinate::from_dense_root(penalty_root)],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![1.0],
second: None,
},
deriv_provider: Box::new(GaussianDerivatives),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 10,
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
},
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
};
let rho = [lambda.ln()];
let gradient_at_optimum = [0.0_f64];
let steps = compute_efs_update(&solution, &rho, &gradient_at_optimum);
assert_eq!(steps.len(), 1);
assert!(
steps[0].abs() < 1e-12,
"EFS step at scalar optimum should be exactly 0, got {} (old buggy formula returned ~+5)",
steps[0]
);
let q_eff = lambda * beta_hat * beta_hat; let g_off = 0.1_f64;
let steps_off = compute_efs_update(&solution, &rho, &[g_off]);
let expected = (1.0_f64 - 2.0 * g_off / q_eff).ln();
assert!(
(steps_off[0] - expected).abs() < 1e-12,
"off-optimum EFS step {} != expected {}",
steps_off[0],
expected
);
}
#[test]
pub(crate) fn efs_log_step_from_grad_recovers_canonical_form() {
let cases = [
(1.0_f64, 0.5),
(2.0, 1.5),
(0.75, 0.75),
(4.0, 0.1),
(1.0, 0.999),
];
for (q_eff, target) in cases {
let g_base = (q_eff - target) / 2.0;
let universal = efs_log_step_from_grad(q_eff, g_base).unwrap();
let canonical = (target / q_eff).ln().clamp(-EFS_MAX_STEP, EFS_MAX_STEP);
assert!(
(universal - canonical).abs() < 1e-12,
"universal {universal} ≠ canonical {canonical} at q={q_eff}, t={target}"
);
}
let target = 0.6_f64;
let g_extra = -0.7_f64;
let augmented_q = target - 2.0 * g_extra;
let g_full_at_aug_opt = (augmented_q - target) / 2.0 + g_extra;
assert!(g_full_at_aug_opt.abs() < 1e-12);
let s_at_opt = efs_log_step_from_grad(augmented_q, g_full_at_aug_opt).unwrap();
assert!(
s_at_opt.abs() < 1e-12,
"Δρ at augmented optimum != 0: {s_at_opt}"
);
let s = efs_log_step_from_grad(2.0, 0.75).expect("stable regime");
assert!((s - (0.25_f64).ln()).abs() < 1e-12);
let s = efs_log_step_from_grad(0.75, 0.0).expect("zero gradient");
assert!(s.abs() < 1e-12);
for &(q_eff, g) in &[(1.0_f64, 0.6), (2.0, 1.5), (0.5, 1e6)] {
let s = efs_log_step_from_grad(q_eff, g).expect("over-correction");
assert!((s - (-EFS_MAX_STEP)).abs() < 1e-12);
}
let s = efs_log_step_from_grad(1.0, 0.5 - 1e-30).expect("near-singular");
assert!((s - (-EFS_MAX_STEP)).abs() < 1e-12 || s == 0.5 * (-EFS_MAX_STEP) || s.is_finite());
assert!(s <= 0.0);
assert!(efs_log_step_from_grad(0.0, 0.0).is_none());
assert!(efs_log_step_from_grad(-1.0, 0.0).is_none());
assert!(efs_log_step_from_grad(f64::NAN, 0.0).is_none());
assert!(efs_log_step_from_grad(1.0, f64::NAN).is_none());
assert!(efs_log_step_from_grad(1.0, f64::INFINITY).is_none());
}
#[test]
pub(crate) fn dense_spectral_block_local_cross_trace_matches_dense() {
let h = array![[4.0, 1.0, 0.5], [1.0, 3.0, 0.25], [0.5, 0.25, 2.0],];
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let block = array![[1.5, 0.4], [0.4, 0.7]];
let scale = 1.7_f64;
let mut a_full = Array2::<f64>::zeros((3, 3));
for i in 0..2 {
for j in 0..2 {
a_full[[i, j]] = scale * block[[i, j]];
}
}
let hinva = op.solve_multi(&a_full); let expected = (&hinva.t() * &hinva).sum();
let got = op.trace_hinv_block_local_cross(&block, scale, 0, 2);
assert!(
(got - expected).abs() < 1e-10,
"block-local cross trace = {got}, expected = {expected} (delta {})",
got - expected
);
}
#[test]
pub(crate) fn test_reml_laml_evaluate_gaussian_basic() {
let h = Array2::from_diag(&array![10.0, 8.0]);
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let solution = InnerSolution {
log_likelihood: -5.0, penalty_quadratic: 2.0,
hessian_op: Arc::new(op),
beta: array![1.0, 0.5],
penalty_coords: vec![PenaltyCoordinate::from_dense_root(
Array2::eye(2), )],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![1.0],
second: None,
},
deriv_provider: Box::new(GaussianDerivatives),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 100,
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::ProfiledGaussian,
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
};
let rho = [0.0];
let result = reml_laml_evaluate(&solution, &rho, EvalMode::ValueOnly, None).unwrap();
assert!(result.cost.is_finite());
assert!(result.gradient.is_none());
let result = reml_laml_evaluate(&solution, &rho, EvalMode::ValueAndGradient, None).unwrap();
assert!(result.cost.is_finite());
assert!(result.gradient.is_some());
let grad = result.gradient.unwrap();
assert_eq!(grad.len(), 1);
assert!(grad[0].is_finite());
}
#[test]
pub(crate) fn fixed_dispersion_firth_cost_subtracts_jeffreys_term() {
assert!(file!().ends_with(".rs"));
let x = array![[1.0, 0.0], [1.0, 1.0], [1.0, -1.0]];
let eta = array![0.0, 0.4, -0.2];
let firth_op = std::sync::Arc::new(
super::super::RemlState::build_firth_dense_operator_for_link(
&crate::types::InverseLink::Standard(crate::types::StandardLink::Logit),
&x,
&eta,
ndarray::Array1::ones(x.nrows()).view(),
)
.expect("firth operator"),
);
let firth_value = firth_op.jeffreys_logdet();
let solution = InnerSolution {
log_likelihood: 0.0,
penalty_quadratic: 0.0,
hessian_op: Arc::new(DenseSpectralOperator::from_symmetric(&Array2::eye(2)).unwrap()),
beta: Array1::zeros(2),
penalty_coords: Vec::new(),
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: Array1::zeros(0),
second: None,
},
deriv_provider: Box::new(GaussianDerivatives),
tk_correction: 0.0,
tk_gradient: None,
firth: Some(ExactJeffreysTerm::new(firth_op)),
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: x.nrows(),
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: false,
},
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
};
let result = reml_laml_evaluate(&solution, &[], EvalMode::ValueOnly, None).unwrap();
assert_relative_eq!(result.cost, -firth_value, epsilon = 1e-12);
}
pub(crate) struct FixedOuterHessianOperator {
pub(crate) matrix: Array2<f64>,
}
impl crate::solver::outer_strategy::OuterHessianOperator for FixedOuterHessianOperator {
fn dim(&self) -> usize {
self.matrix.nrows()
}
fn matvec(&self, v: &Array1<f64>) -> Result<Array1<f64>, String> {
if v.len() != self.dim() {
return Err(RemlError::DimensionMismatch {
reason: format!(
"fixed test outer Hessian dimension mismatch: got {}, expected {}",
v.len(),
self.dim()
),
}
.into());
}
Ok(self.matrix.dot(v))
}
fn is_cheap_to_materialize(&self) -> bool {
true
}
}
pub(crate) struct FamilyOperatorDerivatives {
pub(crate) op: Arc<dyn crate::solver::outer_strategy::OuterHessianOperator>,
}
impl HessianDerivativeProvider for FamilyOperatorDerivatives {
fn hessian_derivative_correction(
&self,
arr: &Array1<f64>,
) -> Result<Option<Array2<f64>>, String> {
assert!(arr.iter().all(|v| !v.is_nan()));
panic!("family operator dispatch should not request pairwise first derivatives")
}
fn hessian_second_derivative_correction(
&self,
arr: &Array1<f64>,
arr2: &Array1<f64>,
arr3: &Array1<f64>,
) -> Result<Option<Array2<f64>>, String> {
assert!(arr.iter().all(|v| !v.is_nan()));
assert!(arr2.iter().all(|v| !v.is_nan()));
assert!(arr3.iter().all(|v| !v.is_nan()));
panic!("family operator dispatch should not request pairwise second derivatives")
}
fn has_corrections(&self) -> bool {
false
}
fn family_outer_hessian_operator(
&self,
) -> Option<Arc<dyn crate::solver::outer_strategy::OuterHessianOperator>> {
Some(Arc::clone(&self.op))
}
}
#[test]
pub(crate) fn family_outer_hessian_operator_short_circuits_dense_pairwise_assembly() {
let supplied = array![[2.5]];
let provider_op: Arc<dyn crate::solver::outer_strategy::OuterHessianOperator> =
Arc::new(FixedOuterHessianOperator {
matrix: supplied.clone(),
});
let solution = InnerSolution {
log_likelihood: 0.0,
penalty_quadratic: 0.4,
hessian_op: Arc::new(DenseSpectralOperator::from_symmetric(&array![[3.0]]).unwrap()),
beta: array![0.2],
penalty_coords: vec![PenaltyCoordinate::from_dense_root(array![[1.0]])],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![1.0],
second: Some(array![[0.0]]),
},
deriv_provider: Box::new(FamilyOperatorDerivatives { op: provider_op }),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 1,
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
},
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
};
let result =
reml_laml_evaluate(&solution, &[0.0], EvalMode::ValueGradientHessian, None).unwrap();
let crate::solver::outer_strategy::HessianResult::Operator(op) = result.hessian else {
panic!("expected family-supplied operator Hessian");
};
assert_eq!(op.dim(), 1);
let hv = op.matvec(&array![4.0]).unwrap();
assert_relative_eq!(hv[0], 10.0, epsilon = 1e-12);
let dense = op.materialize_dense().unwrap();
assert_relative_eq!(dense[[0, 0]], supplied[[0, 0]], epsilon = 1e-12);
}
pub(crate) struct FixedCorrectionDerivatives {
pub(crate) correction: Array2<f64>,
}
impl HessianDerivativeProvider for FixedCorrectionDerivatives {
fn hessian_derivative_correction(
&self,
arr: &Array1<f64>,
) -> Result<Option<Array2<f64>>, String> {
assert!(arr.iter().all(|v| !v.is_nan()));
Ok(Some(self.correction.clone()))
}
fn has_corrections(&self) -> bool {
true
}
}
pub(crate) fn build_projected_rho_gradient_solution(rho: f64) -> InnerSolution<'static> {
let lambda = rho.exp();
let h = array![[3.0 + 4.0 * rho, 0.0], [0.0, 5.0 + lambda],];
let full_logdet = h[[0, 0]].ln() + h[[1, 1]].ln();
let projected_logdet = h[[1, 1]].ln();
InnerSolution {
log_likelihood: 0.0,
penalty_quadratic: 0.0,
hessian_op: Arc::new(
DenseSpectralOperator::from_symmetric_with_mode(&h, PseudoLogdetMode::HardPseudo)
.unwrap(),
),
beta: Array1::zeros(2),
penalty_coords: vec![PenaltyCoordinate::from_dense_root(array![[0.0, 1.0]])],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![0.0],
second: None,
},
deriv_provider: Box::new(FixedCorrectionDerivatives {
correction: array![[4.0, 0.0], [0.0, 0.0]],
}),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: projected_logdet - full_logdet,
penalty_subspace_trace: Some(Arc::new(PenaltySubspaceTrace {
u_s: array![[0.0], [1.0]],
h_proj_inverse: array![[1.0 / h[[1, 1]]]],
})),
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 10,
nullspace_dim: 1.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: false,
},
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
}
}
#[test]
pub(crate) fn test_rho_gradient_uses_projected_logdet_kernel_when_available() {
let rho = 0.0;
let result = reml_laml_evaluate(
&build_projected_rho_gradient_solution(rho),
&[rho],
EvalMode::ValueAndGradient,
None,
)
.unwrap();
let analytic = result.gradient.expect("gradient")[0];
let eps = 1e-6;
let rho_plus = rho + eps;
let cost_plus = reml_laml_evaluate(
&build_projected_rho_gradient_solution(rho_plus),
&[rho_plus],
EvalMode::ValueOnly,
None,
)
.unwrap()
.cost;
let rho_minus = rho - eps;
let cost_minus = reml_laml_evaluate(
&build_projected_rho_gradient_solution(rho_minus),
&[rho_minus],
EvalMode::ValueOnly,
None,
)
.unwrap()
.cost;
let fd = (cost_plus - cost_minus) / (2.0 * eps);
assert_relative_eq!(analytic, fd, epsilon = 1e-8, max_relative = 1e-8);
let full_space_trace = 4.0 / 3.0 + 1.0 / 6.0;
assert!(
(analytic - 0.5 * full_space_trace).abs() > 0.5,
"projected rho trace should exclude the null-space leakage term"
);
}
#[test]
pub(crate) fn test_rho_corrections_serial_large_work_case_stays_finite() {
let rho = 0.0;
let mut solution = build_projected_rho_gradient_solution(rho);
solution.n_observations = 40_000_000;
let result = reml_laml_evaluate(&solution, &[rho], EvalMode::ValueAndGradient, None)
.expect("serial rho correction evaluation");
assert!(result.cost.is_finite());
let gradient = result.gradient.expect("gradient");
assert_eq!(gradient.len(), 1);
assert!(gradient[0].is_finite());
}
pub(crate) fn gaussian_penalty_logdet_fd(
p: usize,
s1: &Array2<f64>,
s2: &Array2<f64>,
rho: &[f64],
) -> PenaltyLogdetDerivs {
let mut s_total = Array2::zeros((p, p));
s_total.scaled_add(rho[0].exp(), s1);
s_total.scaled_add(rho[1].exp(), s2);
let (s_eigs, _) = s_total.eigh(faer::Side::Lower).unwrap();
let threshold = positive_eigenvalue_threshold(s_eigs.as_slice().unwrap());
let log_det_s = exact_pseudo_logdet(s_eigs.as_slice().unwrap(), threshold);
let log_det_s_at = |rho_eval: &[f64]| -> f64 {
let lambdas_eval: Vec<f64> = rho_eval.iter().map(|&r| r.exp()).collect();
let mut s_eval = Array2::zeros((p, p));
s_eval.scaled_add(lambdas_eval[0], s1);
s_eval.scaled_add(lambdas_eval[1], s2);
let (s_eigs_eval, _) = s_eval.eigh(faer::Side::Lower).unwrap();
let threshold_eval = positive_eigenvalue_threshold(s_eigs_eval.as_slice().unwrap());
exact_pseudo_logdet(s_eigs_eval.as_slice().unwrap(), threshold_eval)
};
let mut det1 = Array1::zeros(rho.len());
let eps = 1e-7;
for k in 0..rho.len() {
let mut rho_plus = rho.to_vec();
rho_plus[k] += eps;
let log_det_s_plus = log_det_s_at(&rho_plus);
let mut rho_minus = rho.to_vec();
rho_minus[k] -= eps;
let log_det_s_minus = log_det_s_at(&rho_minus);
det1[k] = (log_det_s_plus - log_det_s_minus) / (2.0 * eps);
}
let mut det2 = Array2::zeros((rho.len(), rho.len()));
let eps2 = 1e-5;
for i in 0..rho.len() {
for j in i..rho.len() {
let value = if i == j {
let mut rho_plus = rho.to_vec();
rho_plus[i] += eps2;
let mut rho_minus = rho.to_vec();
rho_minus[i] -= eps2;
(log_det_s_at(&rho_plus) - 2.0 * log_det_s + log_det_s_at(&rho_minus))
/ (eps2 * eps2)
} else {
let mut pp = rho.to_vec();
pp[i] += eps2;
pp[j] += eps2;
let mut pm = rho.to_vec();
pm[i] += eps2;
pm[j] -= eps2;
let mut mp = rho.to_vec();
mp[i] -= eps2;
mp[j] += eps2;
let mut mm = rho.to_vec();
mm[i] -= eps2;
mm[j] -= eps2;
(log_det_s_at(&pp) - log_det_s_at(&pm) - log_det_s_at(&mp) + log_det_s_at(&mm))
/ (4.0 * eps2 * eps2)
};
det2[[i, j]] = value;
if i != j {
det2[[j, i]] = value;
}
}
}
PenaltyLogdetDerivs {
value: log_det_s,
first: det1,
second: Some(det2),
}
}
pub(crate) fn build_gaussian_test_solution(rho: &[f64]) -> InnerSolution<'_> {
let p = 3; let n = 50;
let xtx = array![[10.0, 2.0, 1.0], [2.0, 8.0, 0.5], [1.0, 0.5, 6.0],];
let s1 = array![[1.0, 0.2, 0.0], [0.2, 1.0, 0.0], [0.0, 0.0, 0.0],];
let s2 = array![[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0],];
let lambdas: Vec<f64> = rho.iter().map(|&r| r.exp()).collect();
let mut h = xtx.clone();
h.scaled_add(lambdas[0], &s1);
h.scaled_add(lambdas[1], &s2);
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let xty = array![5.0, 3.0, 2.0];
let beta = op.solve(&xty);
let r1 = penalty_matrix_root(&s1).unwrap();
let r2 = penalty_matrix_root(&s2).unwrap();
let penalty_quad =
lambdas[0] * beta.dot(&s1.dot(&beta)) + lambdas[1] * beta.dot(&s2.dot(&beta));
let yty = 20.0;
let deviance = yty - 2.0 * beta.dot(&xty) + beta.dot(&xtx.dot(&beta));
let log_likelihood = -0.5 * deviance;
let penalty_logdet = gaussian_penalty_logdet_fd(p, &s1, &s2, rho);
InnerSolution {
log_likelihood,
penalty_quadratic: penalty_quad,
hessian_op: Arc::new(op),
beta,
penalty_coords: vec![
PenaltyCoordinate::from_dense_root(r1),
PenaltyCoordinate::from_dense_root(r2),
],
penalty_logdet,
deriv_provider: Box::new(GaussianDerivatives),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: n,
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::ProfiledGaussian,
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
}
}
pub(crate) fn build_large_dense_spectral_gaussian_solution(rho: f64) -> InnerSolution<'static> {
let p = 520usize;
let n = 2 * p;
let lambda = rho.exp();
let xtx_diag = Array1::from_shape_fn(p, |i| 5.0 + 0.01 * (i as f64));
let xtx = Array2::from_diag(&xtx_diag);
let penalty = Array2::<f64>::eye(p);
let mut h = xtx.clone();
h.scaled_add(lambda, &penalty);
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let xty = Array1::from_shape_fn(p, |i| 1.0 + 0.002 * (i as f64));
let beta = op.solve(&xty);
let penalty_quad = lambda * beta.dot(&beta);
let yty = 10.0 * (p as f64);
let deviance = yty - 2.0 * beta.dot(&xty) + beta.dot(&xtx.dot(&beta));
let log_likelihood = -0.5 * deviance;
InnerSolution {
log_likelihood,
penalty_quadratic: penalty_quad,
hessian_op: Arc::new(op),
beta,
penalty_coords: vec![PenaltyCoordinate::from_dense_root(Array2::<f64>::eye(p))],
penalty_logdet: PenaltyLogdetDerivs {
value: (p as f64) * rho,
first: array![p as f64],
second: None,
},
deriv_provider: Box::new(GaussianDerivatives),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: n,
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::ProfiledGaussian,
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
}
}
#[test]
pub(crate) fn test_gaussian_reml_fd_vs_analytic_gradient() {
let rho = vec![1.0, -0.5];
let solution = build_gaussian_test_solution(&rho);
let result = reml_laml_evaluate(&solution, &rho, EvalMode::ValueAndGradient, None).unwrap();
let analytic_grad = result.gradient.unwrap();
let eps = 1e-5;
let mut fd_grad = Array1::zeros(rho.len());
for k in 0..rho.len() {
let mut rho_plus = rho.clone();
rho_plus[k] += eps;
let sol_plus = build_gaussian_test_solution(&rho_plus);
let cost_plus = reml_laml_evaluate(&sol_plus, &rho_plus, EvalMode::ValueOnly, None)
.unwrap()
.cost;
let mut rho_minus = rho.clone();
rho_minus[k] -= eps;
let sol_minus = build_gaussian_test_solution(&rho_minus);
let cost_minus = reml_laml_evaluate(&sol_minus, &rho_minus, EvalMode::ValueOnly, None)
.unwrap()
.cost;
fd_grad[k] = (cost_plus - cost_minus) / (2.0 * eps);
}
for k in 0..rho.len() {
let abs_err = (analytic_grad[k] - fd_grad[k]).abs();
let rel_err = abs_err / (1.0 + analytic_grad[k].abs());
assert!(
rel_err < 1e-4,
"Gradient mismatch at k={}: analytic={:.8e}, fd={:.8e}, rel_err={:.3e}",
k,
analytic_grad[k],
fd_grad[k],
rel_err,
);
}
}
#[test]
pub(crate) fn test_stochastic_trace_estimator_accuracy() {
let h = array![[4.0, 1.0, 0.5], [1.0, 3.0, 0.2], [0.5, 0.2, 2.0],];
let a1 = array![[1.0, 0.3, 0.0], [0.3, 0.5, 0.1], [0.0, 0.1, 0.2],];
let a2 = array![[0.2, 0.0, 0.1], [0.0, 1.0, 0.4], [0.1, 0.4, 0.8],];
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let exact1 = op.trace_hinv_product(&a1);
let exact2 = op.trace_hinv_product(&a2);
let config = StochasticTraceConfig {
n_probes_min: 50,
n_probes_max: 200,
relative_tol: 0.005,
tau_rel: 1e-10,
solve_rel_tol: 1e-8,
seed: 42,
hutchpp_sketch_dim: None,
};
let estimator = StochasticTraceEstimator::new(config);
let matrices: Vec<&Array2<f64>> = vec![&a1, &a2];
let estimates = estimator.estimate_traces(&op, &matrices);
let rel_err1 = (estimates[0] - exact1).abs() / exact1.abs().max(1e-10);
let rel_err2 = (estimates[1] - exact2).abs() / exact2.abs().max(1e-10);
assert!(
rel_err1 < 0.05,
"Stochastic trace 1: est={:.6}, exact={:.6}, rel_err={:.4}",
estimates[0],
exact1,
rel_err1,
);
assert!(
rel_err2 < 0.05,
"Stochastic trace 2: est={:.6}, exact={:.6}, rel_err={:.4}",
estimates[1],
exact2,
rel_err2,
);
}
#[test]
pub(crate) fn modified_gram_schmidt_orthonormalizes_well_conditioned_input() {
let y = array![
[1.0, 2.0, 0.5, 3.0],
[0.0, 1.0, 0.5, 1.5],
[0.0, 0.0, 1.0, 0.5],
[0.0, 0.0, 0.0, 1.0],
];
let mut q = Array2::<f64>::zeros(y.dim());
let rank = modified_gram_schmidt(&y, &mut q);
assert_eq!(rank, 4, "well-conditioned input should retain full rank");
for j in 0..rank {
for k in 0..rank {
let dot = q.column(j).dot(&q.column(k));
let expected = if j == k { 1.0 } else { 0.0 };
assert!(
(dot - expected).abs() < 1e-12,
"QᵀQ off-identity at ({j},{k}): got {dot}",
);
}
}
}
#[test]
pub(crate) fn modified_gram_schmidt_drops_redundant_columns() {
let y = array![
[1.0, 2.0, 1.0, 4.0],
[0.0, 1.0, 0.0, 2.0],
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
];
let mut q = Array2::<f64>::zeros(y.dim());
let rank = modified_gram_schmidt(&y, &mut q);
assert_eq!(
rank, 2,
"two duplicate columns plus a zero-extension should drop to rank 2"
);
for j in 0..rank {
for k in 0..rank {
let dot = q.column(j).dot(&q.column(k));
let expected = if j == k { 1.0 } else { 0.0 };
assert!((dot - expected).abs() < 1e-12);
}
}
}
#[test]
pub(crate) fn hutchpp_estimate_trace_hinv_operator_matches_exact_within_tolerance() {
let h = array![
[4.0, 1.0, 0.5, 0.0, 0.0, 0.0],
[1.0, 3.0, 0.2, 0.0, 0.0, 0.0],
[0.5, 0.2, 2.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 5.0, 0.7, 0.1],
[0.0, 0.0, 0.0, 0.7, 4.0, 0.3],
[0.0, 0.0, 0.0, 0.1, 0.3, 3.0],
];
let m = array![
[1.0, 0.3, 0.0, 0.1, 0.0, 0.0],
[0.3, 0.5, 0.1, 0.0, 0.2, 0.0],
[0.0, 0.1, 0.2, 0.0, 0.0, 0.05],
[0.1, 0.0, 0.0, 0.8, 0.2, 0.0],
[0.0, 0.2, 0.0, 0.2, 0.6, 0.1],
[0.0, 0.0, 0.05, 0.0, 0.1, 0.4],
];
let hop = DenseSpectralOperator::from_symmetric(&h).unwrap();
let m_op = DenseMatrixHyperOperator { matrix: m.clone() };
let exact = hop.trace_hinv_product(&m);
let config = StochasticTraceConfig {
n_probes_min: 12,
n_probes_max: 64,
relative_tol: 0.005,
tau_rel: 1e-10,
solve_rel_tol: 1e-10,
seed: 0xABCDEF,
hutchpp_sketch_dim: Some(3),
};
let est = hutchpp_estimate_trace_hinv_operator(&hop, &m_op, &config);
let rel_err = (est - exact).abs() / exact.abs().max(1e-10);
assert!(
rel_err < 0.05,
"Hutch++ trace est={est:.6} exact={exact:.6} rel_err={rel_err:.4}"
);
let mut config_plain = config.clone();
config_plain.hutchpp_sketch_dim = None;
config_plain.n_probes_max = 64; let est_plain = hutchpp_estimate_trace_hinv_operator(&hop, &m_op, &config_plain);
let rel_err_plain = (est_plain - exact).abs() / exact.abs().max(1e-10);
assert!(
rel_err <= rel_err_plain * 2.0 + 0.01,
"Hutch++ ({rel_err:.4}) should be competitive with Hutchinson ({rel_err_plain:.4})"
);
}
#[test]
pub(crate) fn hutchpp_estimate_trace_hinv_op_squared_matches_exact() {
let h = array![
[4.0, 1.0, 0.5, 0.0, 0.0, 0.0],
[1.0, 3.0, 0.2, 0.0, 0.0, 0.0],
[0.5, 0.2, 2.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 5.0, 0.7, 0.1],
[0.0, 0.0, 0.0, 0.7, 4.0, 0.3],
[0.0, 0.0, 0.0, 0.1, 0.3, 3.0],
];
let a = array![
[1.0, 0.3, 0.0, 0.1, 0.0, 0.0],
[0.3, 0.5, 0.1, 0.0, 0.2, 0.0],
[0.0, 0.1, 0.2, 0.0, 0.0, 0.05],
[0.1, 0.0, 0.0, 0.8, 0.2, 0.0],
[0.0, 0.2, 0.0, 0.2, 0.6, 0.1],
[0.0, 0.0, 0.05, 0.0, 0.1, 0.4],
];
let hop = DenseSpectralOperator::from_symmetric(&h).unwrap();
let a_op = DenseMatrixHyperOperator { matrix: a.clone() };
let exact = hop.trace_hinv_product_cross(&a, &a);
let config = StochasticTraceConfig {
n_probes_min: 16,
n_probes_max: 96,
relative_tol: 0.005,
tau_rel: 1e-10,
solve_rel_tol: 1e-10,
seed: 0xC0FFEE,
hutchpp_sketch_dim: Some(3),
};
let est = hutchpp_estimate_trace_hinv_op_squared(&hop, &a_op, &config);
let rel_err = (est - exact).abs() / exact.abs().max(1e-10);
assert!(
rel_err < 0.05,
"Hutch++ tr((H⁻¹A)²) est={est:.6} exact={exact:.6} rel_err={rel_err:.4}"
);
let estimator = StochasticTraceEstimator::new(config.clone());
let est_wired = estimator.estimate_second_order_single_dense(&hop, &a);
let rel_err_wired = (est_wired - exact).abs() / exact.abs().max(1e-10);
assert!(
rel_err_wired < 0.05,
"wired Hutch++ second-order est={est_wired:.6} exact={exact:.6} rel_err={rel_err_wired:.4}"
);
assert!(
(est_wired - est).abs() <= 1e-12,
"wired path must call hutchpp_estimate_trace_hinv_op_squared with the same seed/config"
);
}
#[test]
pub(crate) fn hutchpp_estimate_trace_hinv_operator_cross_matches_exact() {
let h = array![
[4.0, 1.0, 0.5, 0.0, 0.0, 0.0],
[1.0, 3.0, 0.2, 0.0, 0.0, 0.0],
[0.5, 0.2, 2.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 5.0, 0.7, 0.1],
[0.0, 0.0, 0.0, 0.7, 4.0, 0.3],
[0.0, 0.0, 0.0, 0.1, 0.3, 3.0],
];
let a = array![
[1.0, 0.3, 0.0, 0.1, 0.0, 0.0],
[0.3, 0.5, 0.1, 0.0, 0.2, 0.0],
[0.0, 0.1, 0.2, 0.0, 0.0, 0.05],
[0.1, 0.0, 0.0, 0.8, 0.2, 0.0],
[0.0, 0.2, 0.0, 0.2, 0.6, 0.1],
[0.0, 0.0, 0.05, 0.0, 0.1, 0.4],
];
let b = array![
[0.5, 0.0, 0.1, 0.0, 0.05, 0.0],
[0.0, 0.7, 0.0, 0.2, 0.0, 0.1],
[0.1, 0.0, 0.4, 0.0, 0.15, 0.0],
[0.0, 0.2, 0.0, 0.6, 0.0, 0.05],
[0.05, 0.0, 0.15, 0.0, 0.3, 0.0],
[0.0, 0.1, 0.0, 0.05, 0.0, 0.5],
];
let hop = DenseSpectralOperator::from_symmetric(&h).unwrap();
let a_op = DenseMatrixHyperOperator { matrix: a.clone() };
let b_op = DenseMatrixHyperOperator { matrix: b.clone() };
let exact = hop.trace_hinv_product_cross(&a, &b);
let config = StochasticTraceConfig {
n_probes_min: 16,
n_probes_max: 128,
relative_tol: 0.005,
tau_rel: 1e-10,
solve_rel_tol: 1e-10,
seed: 0xDEAD_BEEF,
hutchpp_sketch_dim: Some(3),
};
let est = hutchpp_estimate_trace_hinv_operator_cross(&hop, &a_op, &b_op, &config);
let rel_err = (est - exact).abs() / exact.abs().max(1e-10);
assert!(
rel_err < 0.07,
"Hutch++ cross trace est={est:.6} exact={exact:.6} rel_err={rel_err:.4}"
);
}
#[test]
pub(crate) fn trace_hinv_operator_cross_default_routes_implicit_to_hutchpp() {
let p = 200usize;
let mut h = Array2::<f64>::zeros((p, p));
for i in 0..p {
h[[i, i]] = 5.0 + (i as f64) * 0.01;
if i + 1 < p {
h[[i, i + 1]] = 0.2;
h[[i + 1, i]] = 0.2;
}
}
let mut a = Array2::<f64>::zeros((p, p));
for i in 0..p {
a[[i, i]] = 1.0 + 0.005 * (i as f64);
if i + 2 < p {
a[[i, i + 2]] = 0.1;
a[[i + 2, i]] = 0.1;
}
}
let hop = DenseSpectralOperator::from_symmetric(&h).unwrap();
struct ImplicitDense(Array2<f64>);
impl HyperOperator for ImplicitDense {
fn dim(&self) -> usize {
self.0.nrows()
}
fn mul_vec(&self, v: &Array1<f64>) -> Array1<f64> {
let mut out = Array1::<f64>::zeros(self.0.nrows());
dense_matvec_into(&self.0, v.view(), out.view_mut());
out
}
fn mul_vec_into(&self, v: ArrayView1<'_, f64>, out: ArrayViewMut1<'_, f64>) {
dense_matvec_into(&self.0, v, out);
}
fn to_dense(&self) -> Array2<f64> {
self.0.clone()
}
fn is_implicit(&self) -> bool {
true
}
}
let a_op = ImplicitDense(a.clone());
let exact = hop.trace_hinv_product_cross(&a, &a);
let est_same = hop.trace_hinv_operator_cross(&a_op, &a_op);
assert!(est_same.is_finite(), "cross trace must be finite");
let rel_err_same = (est_same - exact).abs() / exact.abs().max(1e-10);
assert!(
rel_err_same < 0.10,
"default same-op cross routing est={est_same:.6} exact={exact:.6} rel_err={rel_err_same:.4}"
);
let mut b = Array2::<f64>::zeros((p, p));
for i in 0..p {
b[[i, i]] = 0.6 + 0.003 * (i as f64);
if i + 1 < p {
b[[i, i + 1]] = 0.05;
b[[i + 1, i]] = 0.05;
}
}
let b_op = ImplicitDense(b.clone());
let exact_ab = hop.trace_hinv_product_cross(&a, &b);
let est_ab = hop.trace_hinv_operator_cross(&a_op, &b_op);
assert!(est_ab.is_finite(), "cross trace (a,b) must be finite");
let rel_err_ab = (est_ab - exact_ab).abs() / exact_ab.abs().max(1e-10);
assert!(
rel_err_ab < 0.10,
"default distinct-op cross routing est={est_ab:.6} exact={exact_ab:.6} rel_err={rel_err_ab:.4}"
);
let exact_ma = hop.trace_hinv_product_cross(&a, &b);
let est_ma = hop.trace_hinv_matrix_operator_cross(&a, &b_op);
assert!(est_ma.is_finite(), "matrix-op cross trace must be finite");
let rel_err_ma = (est_ma - exact_ma).abs() / exact_ma.abs().max(1e-10);
assert!(
rel_err_ma < 0.10,
"default matrix-operator cross routing est={est_ma:.6} exact={exact_ma:.6} rel_err={rel_err_ma:.4}"
);
}
#[test]
pub(crate) fn dense_spectral_large_p_outer_gradient_matches_finite_difference() {
let rho = 0.2;
let solution = build_large_dense_spectral_gaussian_solution(rho);
let result = reml_laml_evaluate(&solution, &[rho], EvalMode::ValueAndGradient, None).unwrap();
let analytic = result.gradient.expect("gradient")[0];
let eps = 1e-5;
let rho_plus = rho + eps;
let solution_plus = build_large_dense_spectral_gaussian_solution(rho_plus);
let cost_plus = reml_laml_evaluate(&solution_plus, &[rho_plus], EvalMode::ValueOnly, None)
.unwrap()
.cost;
let rho_minus = rho - eps;
let solution_minus = build_large_dense_spectral_gaussian_solution(rho_minus);
let cost_minus = reml_laml_evaluate(&solution_minus, &[rho_minus], EvalMode::ValueOnly, None)
.unwrap()
.cost;
let fd = (cost_plus - cost_minus) / (2.0 * eps);
let rel_err = (analytic - fd).abs() / (1.0 + analytic.abs());
assert!(
rel_err < 2e-4,
"large-p dense spectral gradient mismatch: analytic={analytic:.8e}, fd={fd:.8e}, rel_err={rel_err:.3e}"
);
}
#[test]
pub(crate) fn dense_spectral_logdet_traces_do_not_claim_hinv_kernel_equivalence() {
let h = array![[4.0, 1.0], [1.0, 3.0]];
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
assert!(!op.prefers_stochastic_trace_estimation());
assert!(!op.logdet_traces_match_hinv_kernel());
assert!(!can_use_stochastic_logdet_hinv_kernel(&op, 1024, true));
let block =
BlockCoupledOperator::from_joint_hessian_with_mode(&h, PseudoLogdetMode::Smooth).unwrap();
assert!(!block.prefers_stochastic_trace_estimation());
assert!(!block.logdet_traces_match_hinv_kernel());
assert!(!can_use_stochastic_logdet_hinv_kernel(&block, 1024, true));
}
#[test]
pub(crate) fn dense_spectral_hinv_cross_matches_solve_contraction() {
assert!(file!().ends_with(".rs"));
let h = array![[4.0, 1.0, 0.5], [1.0, 3.0, 0.25], [0.5, 0.25, 2.0],];
let a = array![[1.0, 0.2, 0.1], [0.2, 0.5, 0.0], [0.1, 0.0, 0.3],];
let b = array![[0.3, 0.1, 0.0], [0.1, 0.8, 0.2], [0.0, 0.2, 0.6],];
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let exact = op.trace_hinv_product_cross(&a, &b);
let solved_a = op.solve_multi(&a);
let solved_b = op.solve_multi(&b);
let reference = (&solved_a.t() * &solved_b).sum();
assert_relative_eq!(exact, reference, epsilon = 1e-10, max_relative = 1e-10);
}
#[test]
pub(crate) fn dense_spectral_batched_logdet_crosses_match_pairwise() {
assert!(file!().ends_with(".rs"));
let h = array![[4.0, 1.0, 0.5], [1.0, 3.0, 0.25], [0.5, 0.25, 2.0],];
let h1 = array![[1.0, 0.2, 0.1], [0.2, 0.5, 0.0], [0.1, 0.0, 0.3],];
let h2 = array![[0.3, 0.1, 0.0], [0.1, 0.8, 0.2], [0.0, 0.2, 0.6],];
let h3 = array![[0.7, 0.0, 0.2], [0.0, 0.4, 0.1], [0.2, 0.1, 0.9],];
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let mats = [&h1, &h2, &h3];
let batched = op.trace_logdet_hessian_crosses(&mats);
for i in 0..mats.len() {
for j in 0..mats.len() {
let pairwise = op.trace_logdet_hessian_cross(mats[i], mats[j]);
assert_relative_eq!(
batched[[i, j]],
pairwise,
epsilon = 1e-10,
max_relative = 1e-10
);
}
}
}
#[test]
pub(crate) fn sparse_block_local_trace_without_takahashi_matches_dense_reference() {
assert!(file!().ends_with(".rs"));
let h = array![
[5.0, 0.2, 0.0, 0.1],
[0.2, 4.0, 0.3, 0.0],
[0.0, 0.3, 3.0, 0.4],
[0.1, 0.0, 0.4, 2.5],
];
let h_sparse = crate::linalg::sparse_exact::dense_to_sparse_symmetric_upper(&h, 0.0).unwrap();
let factor =
std::sync::Arc::new(crate::linalg::sparse_exact::factorize_sparse_spd(&h_sparse).unwrap());
let sparse = SparseCholeskyOperator::new(factor, 0.0, h.nrows());
let dense = DenseSpectralOperator::from_symmetric(&h).unwrap();
let block = array![[0.8, 0.15], [0.15, 0.45]];
let scale = 1.7;
let start = 1;
let end = 3;
let mut full = Array2::<f64>::zeros(h.raw_dim());
for i in 0..block.nrows() {
for j in 0..block.ncols() {
full[[start + i, start + j]] = scale * block[[i, j]];
}
}
assert_relative_eq!(
sparse.trace_hinv_block_local(&block, scale, start, end),
dense.trace_hinv_product(&full),
epsilon = 1e-10,
max_relative = 1e-10
);
assert_relative_eq!(
sparse.trace_hinv_block_local_cross(&block, scale, start, end),
dense.trace_hinv_product_cross(&full, &full),
epsilon = 1e-10,
max_relative = 1e-10
);
}
#[test]
pub(crate) fn sparse_block_local_operator_cross_without_takahashi_matches_dense_reference() {
assert!(file!().ends_with(".rs"));
let h = array![
[5.0, 0.2, 0.0, 0.1],
[0.2, 4.0, 0.3, 0.0],
[0.0, 0.3, 3.0, 0.4],
[0.1, 0.0, 0.4, 2.5],
];
let h_sparse = crate::linalg::sparse_exact::dense_to_sparse_symmetric_upper(&h, 0.0).unwrap();
let factor =
std::sync::Arc::new(crate::linalg::sparse_exact::factorize_sparse_spd(&h_sparse).unwrap());
let sparse = SparseCholeskyOperator::new(factor, 0.0, h.nrows());
let dense = DenseSpectralOperator::from_symmetric(&h).unwrap();
let local = array![[0.8, 0.15], [0.15, 0.45]];
let start = 1;
let end = 3;
let op = BlockLocalDrift {
local: local.clone(),
start,
end,
total_dim: h.nrows(),
};
let mut full = Array2::<f64>::zeros(h.raw_dim());
full.slice_mut(ndarray::s![start..end, start..end])
.assign(&local);
assert_relative_eq!(
sparse.trace_hinv_operator_cross(&op, &op),
dense.trace_hinv_product_cross(&full, &full),
epsilon = 1e-10,
max_relative = 1e-10
);
}
#[test]
pub(crate) fn sparse_matrix_block_operator_cross_without_takahashi_matches_dense_reference() {
assert!(file!().ends_with(".rs"));
let h = array![
[5.0, 0.2, 0.0, 0.1],
[0.2, 4.0, 0.3, 0.0],
[0.0, 0.3, 3.0, 0.4],
[0.1, 0.0, 0.4, 2.5],
];
let h_sparse = crate::linalg::sparse_exact::dense_to_sparse_symmetric_upper(&h, 0.0).unwrap();
let factor =
std::sync::Arc::new(crate::linalg::sparse_exact::factorize_sparse_spd(&h_sparse).unwrap());
let sparse = SparseCholeskyOperator::new(factor, 0.0, h.nrows());
let dense = DenseSpectralOperator::from_symmetric(&h).unwrap();
let matrix = array![
[1.0, 0.2, -0.1, 0.3],
[0.2, 0.7, 0.4, -0.2],
[-0.1, 0.4, 1.2, 0.5],
[0.3, -0.2, 0.5, 0.9],
];
let local = array![[0.8, 0.15], [0.15, 0.45]];
let start = 1;
let end = 3;
let op = BlockLocalDrift {
local: local.clone(),
start,
end,
total_dim: h.nrows(),
};
let mut full = Array2::<f64>::zeros(h.raw_dim());
full.slice_mut(ndarray::s![start..end, start..end])
.assign(&local);
assert_relative_eq!(
sparse.trace_hinv_matrix_operator_cross(&matrix, &op),
dense.trace_hinv_product_cross(&matrix, &full),
epsilon = 1e-10,
max_relative = 1e-10
);
}
#[test]
pub(crate) fn sparse_takahashi_trace_hinv_product_pairs_symmetric_lookups() {
assert!(file!().ends_with(".rs"));
let h = array![[4.0, 0.2, 0.1], [0.2, 3.0, 0.4], [0.1, 0.4, 2.5],];
let h_sparse = crate::linalg::sparse_exact::dense_to_sparse_symmetric_upper(&h, 0.0).unwrap();
let factor =
std::sync::Arc::new(crate::linalg::sparse_exact::factorize_sparse_spd(&h_sparse).unwrap());
let sfactor = crate::linalg::sparse_exact::factorize_simplicial(&h_sparse).unwrap();
let taka = std::sync::Arc::new(
crate::linalg::sparse_exact::TakahashiInverse::compute(&sfactor).unwrap(),
);
let sparse = SparseCholeskyOperator::new(factor, 0.0, h.nrows()).with_takahashi(taka);
let dense = DenseSpectralOperator::from_symmetric(&h).unwrap();
let a = array![[1.0, 0.7, -0.2], [0.1, 0.5, 0.9], [0.4, -0.3, 0.2],];
assert_relative_eq!(
sparse.trace_hinv_product(&a),
dense.trace_hinv_product(&a),
epsilon = 1e-10,
max_relative = 1e-10
);
}
#[test]
pub(crate) fn hyper_operator_bilinear_view_matches_owned_bilinear() {
assert!(file!().ends_with(".rs"));
let dense = DenseMatrixHyperOperator {
matrix: array![[2.0, 0.3, -0.1], [0.3, 1.5, 0.4], [-0.1, 0.4, 3.0],],
};
let block = BlockLocalDrift {
local: array![[1.2, 0.2], [0.2, 0.7]],
start: 1,
end: 3,
total_dim: 3,
};
let composite = CompositeHyperOperator {
dense: Some(array![[0.4, 0.1, 0.0], [0.1, 0.8, -0.2], [0.0, -0.2, 0.6],]),
operators: vec![Arc::new(block.clone())],
dim_hint: 3,
};
let weighted = WeightedHyperOperator {
terms: vec![
(1.7, Arc::new(dense.clone()) as Arc<dyn HyperOperator>),
(-0.4, Arc::new(block.clone()) as Arc<dyn HyperOperator>),
],
dim_hint: 3,
};
let v_storage = array![9.0, 0.5, -1.2, 0.7, 8.0];
let u_storage = array![7.0, -0.3, 1.1, 0.9, 6.0];
let v_view = v_storage.slice(ndarray::s![1..4]);
let u_view = u_storage.slice(ndarray::s![1..4]);
let v_owned = v_view.to_owned();
let u_owned = u_view.to_owned();
let operators: [&dyn HyperOperator; 4] = [&dense, &block, &composite, &weighted];
for op in operators {
assert_relative_eq!(
op.bilinear_view(v_view, u_view),
op.bilinear(&v_owned, &u_owned),
epsilon = 1e-12,
max_relative = 1e-12
);
}
}
#[test]
pub(crate) fn hyper_operator_scaled_add_mul_vec_matches_owned_matvec() {
assert!(file!().ends_with(".rs"));
let dense = DenseMatrixHyperOperator {
matrix: array![[2.0, 0.3, -0.1], [0.3, 1.5, 0.4], [-0.1, 0.4, 3.0],],
};
let block = BlockLocalDrift {
local: array![[1.2, 0.2], [0.2, 0.7]],
start: 1,
end: 3,
total_dim: 3,
};
let composite = CompositeHyperOperator {
dense: Some(array![[0.4, 0.1, 0.0], [0.1, 0.8, -0.2], [0.0, -0.2, 0.6],]),
operators: vec![Arc::new(block.clone())],
dim_hint: 3,
};
let weighted = WeightedHyperOperator {
terms: vec![
(1.7, Arc::new(dense.clone()) as Arc<dyn HyperOperator>),
(-0.4, Arc::new(block.clone()) as Arc<dyn HyperOperator>),
(0.0, Arc::new(composite.clone()) as Arc<dyn HyperOperator>),
],
dim_hint: 3,
};
let v_storage = array![9.0, 0.5, -1.2, 0.7, 8.0];
let v_view = v_storage.slice(ndarray::s![1..4]);
let v_owned = v_view.to_owned();
let base = array![0.25, -0.5, 1.5];
let scale = -1.3;
let operators: [&dyn HyperOperator; 4] = [&dense, &block, &composite, &weighted];
for op in operators {
let mut accumulated = base.clone();
op.scaled_add_mul_vec(v_view, scale, accumulated.view_mut());
let mut expected = base.clone();
expected.scaled_add(scale, &op.mul_vec(&v_owned));
for idx in 0..accumulated.len() {
assert_relative_eq!(
accumulated[idx],
expected[idx],
epsilon = 1e-12,
max_relative = 1e-12
);
}
}
}
#[test]
pub(crate) fn stochastic_single_second_order_estimators_match_batched_paths() {
assert!(file!().ends_with(".rs"));
let diag = array![4.0, 3.0, 2.0];
let hop = MatrixFreeSpdOperator::new_with_mode(
diag.len(),
move |v| &diag * v,
PseudoLogdetMode::Smooth,
);
let estimator = StochasticTraceEstimator::with_defaults();
let dense = array![[0.8, 0.2, 0.0], [0.2, 0.5, 0.1], [0.0, 0.1, 0.7],];
let op = DenseMatrixHyperOperator {
matrix: dense.clone(),
};
let no_ops: [&dyn HyperOperator; 0] = [];
let dense_refs = [&dense];
let batched_dense =
estimator.estimate_second_order_traces_with_operators(&hop, &dense_refs, &no_ops);
assert_relative_eq!(
estimator.estimate_second_order_single_dense(&hop, &dense),
batched_dense[[0, 0]],
epsilon = 1e-12,
max_relative = 1e-12
);
let no_dense: [&Array2<f64>; 0] = [];
let op_refs: [&dyn HyperOperator; 1] = [&op];
let batched_op =
estimator.estimate_second_order_traces_with_operators(&hop, &no_dense, &op_refs);
assert_relative_eq!(
estimator.estimate_second_order_single_operator(&hop, &op),
batched_op[[0, 0]],
epsilon = 1e-12,
max_relative = 1e-12
);
}
#[test]
pub(crate) fn matrix_free_logdet_traces_use_exact_spectral_algebra() {
let diag = array![4.0, 3.0, 2.0];
let h = Array2::from_diag(&diag);
let dense = DenseSpectralOperator::from_symmetric(&h).unwrap();
let op = MatrixFreeSpdOperator::new_with_mode(
diag.len(),
move |v| &diag * v,
PseudoLogdetMode::Smooth,
);
let a = array![[0.7, 0.1, 0.0], [0.1, 0.4, 0.2], [0.0, 0.2, 0.5]];
assert_relative_eq!(op.logdet(), dense.logdet(), epsilon = 1e-12);
assert_relative_eq!(
op.trace_hinv_product(&a),
dense.trace_hinv_product(&a),
epsilon = 1e-12
);
assert_relative_eq!(
op.trace_logdet_hessian_cross(&a, &a),
dense.trace_logdet_hessian_cross(&a, &a),
epsilon = 1e-12
);
assert!(!op.prefers_stochastic_trace_estimation());
assert!(!op.logdet_traces_match_hinv_kernel());
assert!(!can_use_stochastic_logdet_hinv_kernel(&op, 1024, true));
assert!(!can_use_stochastic_logdet_hinv_kernel(&op, 128, true));
assert!(!can_use_stochastic_logdet_hinv_kernel(&op, 1024, false));
}
#[test]
pub(crate) fn test_rademacher_probe_properties() {
let mut rng = Xoshiro256SS::from_seed(99);
let mut z = Array1::zeros(100);
rademacher_probe_into(z.view_mut(), &mut rng);
assert_eq!(z.len(), 100);
for &v in z.iter() {
assert!(v == 1.0 || v == -1.0, "Rademacher entry must be +/-1");
}
let mut rng2 = Xoshiro256SS::from_seed(99);
let mut z2 = Array1::zeros(100);
rademacher_probe_into(z2.view_mut(), &mut rng2);
assert_eq!(z, z2, "Same seed must produce identical probes");
}
#[test]
pub(crate) fn test_spectral_logdet_gradient_fd() {
let t0 = 0.0_f64;
let h_step = 1e-6;
let dh_dt = Array2::from_diag(&array![1.0, 2.0, -1.0]);
let h0 = Array2::from_diag(&array![2.0 + t0, 0.01 + 2.0 * t0, 3.0 - t0]);
let op0 = DenseSpectralOperator::from_symmetric(&h0).unwrap();
let analytic = op0.trace_logdet_gradient(&dh_dt);
let h_plus = Array2::from_diag(&array![
2.0 + t0 + h_step,
0.01 + 2.0 * (t0 + h_step),
3.0 - (t0 + h_step)
]);
let h_minus = Array2::from_diag(&array![
2.0 + t0 - h_step,
0.01 + 2.0 * (t0 - h_step),
3.0 - (t0 - h_step)
]);
let op_plus = DenseSpectralOperator::from_symmetric(&h_plus).unwrap();
let op_minus = DenseSpectralOperator::from_symmetric(&h_minus).unwrap();
let fd = (op_plus.logdet() - op_minus.logdet()) / (2.0 * h_step);
let rel_err = (analytic - fd).abs() / fd.abs().max(1e-12);
assert!(
rel_err < 1e-5,
"Spectral logdet gradient mismatch: analytic={:.10e}, fd={:.10e}, rel_err={:.3e}",
analytic,
fd,
rel_err,
);
}
pub(crate) fn rotating_nullspace_penalty(psi: f64, s1: f64, s2: f64) -> Array2<f64> {
let c = psi.cos();
let s = psi.sin();
let r = array![[c, 0.0, -s], [0.0, 1.0, 0.0], [s, 0.0, c],];
let d = Array2::from_diag(&array![s1, s2, 0.0]);
r.dot(&d).dot(&r.t())
}
pub(crate) fn pseudo_logdet(s: &Array2<f64>, tol: f64) -> f64 {
let (eigs, _) = s.eigh(faer::Side::Lower).unwrap();
eigs.iter().filter(|&&v| v > tol).map(|v| v.ln()).sum()
}
pub(crate) fn pseudo_logdet_fd_first(psi: f64, h: f64, s1: f64, s2: f64, tol: f64) -> f64 {
let sp = rotating_nullspace_penalty(psi + h, s1, s2);
let sm = rotating_nullspace_penalty(psi - h, s1, s2);
(pseudo_logdet(&sp, tol) - pseudo_logdet(&sm, tol)) / (2.0 * h)
}
pub(crate) fn pseudo_logdet_fd_second(psi: f64, h: f64, s1: f64, s2: f64, tol: f64) -> f64 {
let sp = pseudo_logdet(&rotating_nullspace_penalty(psi + h, s1, s2), tol);
let s0 = pseudo_logdet(&rotating_nullspace_penalty(psi, s1, s2), tol);
let sm = pseudo_logdet(&rotating_nullspace_penalty(psi - h, s1, s2), tol);
(sp - 2.0 * s0 + sm) / (h * h)
}
pub(crate) fn analytic_pseudo_logdet_second(psi: f64, s1: f64, s2: f64, tol: f64) -> (f64, f64) {
let s_mat = rotating_nullspace_penalty(psi, s1, s2);
let (eigs, vecs) = s_mat.eigh(faer::Side::Lower).unwrap();
let p = eigs.len();
let pos_idx: Vec<usize> = (0..p).filter(|&i| eigs[i] > tol).collect();
let null_idx: Vec<usize> = (0..p).filter(|&i| eigs[i] <= tol).collect();
let c = psi.cos();
let s = psi.sin();
let r = array![[c, 0.0, -s], [0.0, 1.0, 0.0], [s, 0.0, c],];
let rp = array![[-s, 0.0, -c], [0.0, 0.0, 0.0], [c, 0.0, -s],];
let d = Array2::from_diag(&array![s1, s2, 0.0]);
let s_psi = rp.dot(&d).dot(&r.t()) + r.dot(&d).dot(&rp.t());
let rpp = array![[-c, 0.0, s], [0.0, 0.0, 0.0], [-s, 0.0, -c],];
let s_psi_psi =
rpp.dot(&d).dot(&r.t()) + 2.0 * &rp.dot(&d).dot(&rp.t()) + r.dot(&d).dot(&rpp.t());
let mut s_dag = Array2::<f64>::zeros((p, p));
for &i in &pos_idx {
let col = vecs.column(i);
for r in 0..p {
for c2 in 0..p {
s_dag[[r, c2]] += col[r] * col[c2] / eigs[i];
}
}
}
let sdag_s_psi = s_dag.dot(&s_psi);
let term_linear = trace_mat(&s_dag.dot(&s_psi_psi));
let term_quad = trace_mat(&sdag_s_psi.dot(&sdag_s_psi));
let without_correction = term_linear - term_quad;
let mut correction = 0.0_f64;
if !pos_idx.is_empty() && !null_idx.is_empty() {
let n_pos = pos_idx.len();
let n_null = null_idx.len();
let mut u_pos = Array2::<f64>::zeros((p, n_pos));
let mut u_null = Array2::<f64>::zeros((p, n_null));
for (out, &idx) in pos_idx.iter().enumerate() {
u_pos.column_mut(out).assign(&vecs.column(idx));
}
for (out, &idx) in null_idx.iter().enumerate() {
u_null.column_mut(out).assign(&vecs.column(idx));
}
let l_mat = u_pos.t().dot(&s_psi.dot(&u_null));
for a in 0..n_pos {
let sigma_inv_sq = 1.0 / (eigs[pos_idx[a]] * eigs[pos_idx[a]]);
correction += sigma_inv_sq * l_mat.row(a).dot(&l_mat.row(a));
}
correction *= 2.0;
}
let with_correction = without_correction + correction;
(with_correction, without_correction)
}
pub(crate) fn trace_mat(a: &Array2<f64>) -> f64 {
(0..a.nrows()).map(|i| a[[i, i]]).sum()
}
#[test]
pub(crate) fn test_moving_nullspace_correction_needed() {
let s1 = 4.0;
let s2 = 1.0;
let psi = 0.3; let tol = 1e-10;
let h = 1e-5;
let fd_first = pseudo_logdet_fd_first(psi, h, s1, s2, tol);
assert!(
fd_first.is_finite() && fd_first.abs() < 1e-8,
"First derivative should vanish for rotating nullspace, got {fd_first}"
);
let fd_second = pseudo_logdet_fd_second(psi, h, s1, s2, tol);
let (with_corr, without_corr) = analytic_pseudo_logdet_second(psi, s1, s2, tol);
let rel_err_with = (with_corr - fd_second).abs() / fd_second.abs().max(1e-12);
assert!(
rel_err_with < 1e-4,
"With correction: analytic={:.8e}, fd={:.8e}, rel_err={:.3e}",
with_corr,
fd_second,
rel_err_with,
);
let rel_err_without = (without_corr - fd_second).abs() / fd_second.abs().max(1e-12);
assert!(
rel_err_without > 1e-2,
"Without correction should disagree with FD: \
without={:.8e}, fd={:.8e}, rel_err={:.3e} (expected > 1e-2)",
without_corr,
fd_second,
rel_err_without,
);
}
#[test]
pub(crate) fn test_fixed_nullspace_correction_vanishes() {
let tol = 1e-10;
let h = 1e-5;
let rho1 = 0.5_f64;
let rho2 = -0.3_f64;
let build_s = |t: f64| -> Array2<f64> {
Array2::from_diag(&array![(rho1 + t).exp(), (rho2 + 2.0 * t).exp(), 0.0])
};
let t0 = 0.0_f64;
let ld_plus = pseudo_logdet(&build_s(t0 + h), tol);
let ld_0 = pseudo_logdet(&build_s(t0), tol);
let ld_minus = pseudo_logdet(&build_s(t0 - h), tol);
let fd_second = (ld_plus - 2.0 * ld_0 + ld_minus) / (h * h);
let s_mat = build_s(t0);
let s_t = Array2::from_diag(&array![
(rho1 + t0).exp(),
2.0 * (rho2 + 2.0 * t0).exp(),
0.0
]);
let s_tt = Array2::from_diag(&array![
(rho1 + t0).exp(),
4.0 * (rho2 + 2.0 * t0).exp(),
0.0
]);
let (eigs, vecs) = s_mat.eigh(faer::Side::Lower).unwrap();
let p = 3;
let pos_idx: Vec<usize> = (0..p).filter(|&i| eigs[i] > tol).collect();
let null_idx: Vec<usize> = (0..p).filter(|&i| eigs[i] <= tol).collect();
let mut s_dag = Array2::<f64>::zeros((p, p));
for &i in &pos_idx {
let col = vecs.column(i);
for r in 0..p {
for c in 0..p {
s_dag[[r, c]] += col[r] * col[c] / eigs[i];
}
}
}
let sdag_s_t = s_dag.dot(&s_t);
let term_linear = trace_mat(&s_dag.dot(&s_tt));
let term_quad = trace_mat(&sdag_s_t.dot(&sdag_s_t));
let without_correction = term_linear - term_quad;
let mut correction = 0.0_f64;
if !pos_idx.is_empty() && !null_idx.is_empty() {
let n_pos = pos_idx.len();
let n_null = null_idx.len();
let mut u_pos = Array2::<f64>::zeros((p, n_pos));
let mut u_null = Array2::<f64>::zeros((p, n_null));
for (out, &idx) in pos_idx.iter().enumerate() {
u_pos.column_mut(out).assign(&vecs.column(idx));
}
for (out, &idx) in null_idx.iter().enumerate() {
u_null.column_mut(out).assign(&vecs.column(idx));
}
let l_mat = u_pos.t().dot(&s_t.dot(&u_null));
for a in 0..n_pos {
let sigma_inv_sq = 1.0 / (eigs[pos_idx[a]] * eigs[pos_idx[a]]);
correction += sigma_inv_sq * l_mat.row(a).dot(&l_mat.row(a));
}
correction *= 2.0;
}
assert!(
correction.abs() < 1e-12,
"Correction should vanish for fixed nullspace, got {:.3e}",
correction,
);
let with_correction = without_correction + correction;
let abs_err_with = (with_correction - fd_second).abs();
let abs_err_without = (without_correction - fd_second).abs();
assert!(
abs_err_with < 1e-4,
"With correction should match FD: with={:.8e}, fd={:.8e}, abs_err={:.3e}",
with_correction,
fd_second,
abs_err_with,
);
assert!(
abs_err_without < 1e-4,
"Without correction should also match FD (fixed nullspace): \
without={:.8e}, fd={:.8e}, abs_err={:.3e}",
without_correction,
fd_second,
abs_err_without,
);
}
#[test]
pub(crate) fn test_symmetric_eigen_identity() {
let eye = Array2::<f64>::eye(3);
let (evals, evecs) = symmetric_eigen(&eye);
for &e in &evals {
assert!((e - 1.0).abs() < 1e-12, "eigenvalue should be 1.0, got {e}");
}
let prod = evecs.t().dot(&evecs);
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(prod[[i, j]] - expected).abs() < 1e-12,
"Q^T Q should be identity"
);
}
}
}
#[test]
pub(crate) fn test_symmetric_eigen_diagonal() {
let mut d = Array2::<f64>::zeros((3, 3));
d[[0, 0]] = 4.0;
d[[1, 1]] = 2.0;
d[[2, 2]] = 1.0;
let (evals, _) = symmetric_eigen(&d);
let mut sorted = evals.clone();
sorted.sort_by(|a, b| a.total_cmp(b));
assert!((sorted[0] - 1.0).abs() < 1e-12);
assert!((sorted[1] - 2.0).abs() < 1e-12);
assert!((sorted[2] - 4.0).abs() < 1e-12);
}
#[test]
pub(crate) fn test_pseudoinverse_times_vec_identity() {
let eye = Array2::<f64>::eye(3);
let v = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let result = pseudoinverse_times_vec(&eye, v.as_slice().expect("contiguous test vector"), 1e-8);
for i in 0..3 {
assert!((result[i] - v[i]).abs() < 1e-12, "G=I: G⁺v should equal v");
}
}
#[test]
pub(crate) fn test_pseudoinverse_times_vec_singular() {
let mut g = Array2::<f64>::zeros((2, 2));
g[[0, 0]] = 1.0;
g[[0, 1]] = 1.0;
g[[1, 0]] = 1.0;
g[[1, 1]] = 1.0;
let v = Array1::from_vec(vec![2.0, 0.0]);
let result = pseudoinverse_times_vec(&g, v.as_slice().expect("contiguous test vector"), 1e-8);
assert!((result[0] - 0.5).abs() < 1e-10);
assert!((result[1] - 0.5).abs() < 1e-10);
}
#[test]
pub(crate) fn batched_implicit_trace_matches_per_operator_trace() {
use crate::terms::basis::ImplicitDesignPsiDerivative;
use std::sync::Arc;
let n = 5usize;
let p = 3usize;
let n_axes = 2usize;
let len = n * p;
let phi_values = Array1::from_vec((0..len).map(|i| 0.2 + 0.03 * i as f64).collect());
let q_values = Array1::from_vec((0..len).map(|i| -0.4 + 0.05 * i as f64).collect());
let t_values = Array1::zeros(len);
let axis_components = Array2::from_shape_vec(
(len, n_axes),
(0..len)
.flat_map(|i| [0.1 + 0.02 * i as f64, -0.3 + 0.015 * i as f64])
.collect(),
)
.unwrap();
let implicit = Arc::new(ImplicitDesignPsiDerivative::new(
phi_values,
q_values,
t_values,
axis_components,
None,
None,
n,
p,
0,
n_axes,
));
let x_data = array![
[1.0, 0.4, -0.2],
[0.5, 1.1, 0.3],
[-0.3, 0.9, 0.6],
[0.8, -0.5, 1.2],
[0.2, 0.7, -0.4],
];
let x_design = Arc::new(DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(
x_data,
)));
let w_diag = Arc::new(array![1.0, 0.7, 1.3, 0.9, 1.1]);
let h = Array2::<f64>::eye(p);
let ds = DenseSpectralOperator::from_symmetric(&h).unwrap();
let make_op = |axis: usize, scale: f64| -> Arc<dyn HyperOperator> {
Arc::new(ImplicitHyperOperator {
implicit_deriv: Arc::clone(&implicit),
axis,
x_design: Arc::clone(&x_design),
w_diag: crate::matrix::SignedWeightsArc::from_arc(Arc::clone(&w_diag)),
s_psi: Array2::<f64>::eye(p) * scale,
p,
c_x_psi_beta: Some(Arc::new(Array1::from_vec(
(0..n).map(|i| scale * (i as f64 + 1.0)).collect(),
))),
})
};
let ops = vec![make_op(0, 0.05), make_op(1, 0.07)];
let cache = ProjectedFactorCache::default();
let per_operator: Vec<f64> = ops
.iter()
.map(|op| op.trace_projected_factor_cached(&ds.g_factor, &cache))
.collect();
let batched = dense_spectral_trace_logdet_operators_batched(&ds, &ops);
assert_eq!(batched.len(), per_operator.len());
for (want, got) in per_operator.iter().zip(batched.iter()) {
assert_relative_eq!(got, want, epsilon = 1.0e-10, max_relative = 1.0e-10);
}
}
#[test]
pub(crate) fn implicit_hyper_operator_third_derivative_term_matches_dense_reference() {
use crate::terms::basis::ImplicitDesignPsiDerivative;
use std::sync::Arc;
let n = 4usize;
let n_knots = 2usize;
let n_axes = 1usize;
let p = n_knots;
let phi_values = array![1.0, 0.5, 0.7, 0.9, 0.3, 0.4, 0.6, 0.8];
let q_values = array![0.5, -0.2, 0.3, 0.1, -0.4, 0.2, 0.6, -0.1];
let t_values = array![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let axis_components = array![[0.7], [0.3], [-0.4], [0.5], [0.2], [-0.1], [0.6], [0.8]];
let implicit = Arc::new(ImplicitDesignPsiDerivative::new(
phi_values,
q_values,
t_values,
axis_components,
None,
None,
n,
n_knots,
0,
n_axes,
));
let x_data = array![[1.0, 0.30], [0.50, 1.20], [-0.20, 0.80], [0.90, -0.40],];
let x_design = Arc::new(DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(
x_data.clone(),
)));
let w_diag = Arc::new(array![0.8, 1.2, 0.6, 1.5]);
let s_psi = array![[0.40, 0.05], [0.05, 0.25]];
let beta_eval = array![0.30, -0.20];
let c_array = array![0.10, -0.05, 0.20, 0.15];
let dx_dpsi = implicit
.materialize_first(0)
.expect("materialize_first should succeed on tiny fixture");
assert_eq!(dx_dpsi.shape(), &[n, p]);
let dx_beta = dx_dpsi.dot(&beta_eval);
let c_x_psi_beta_dense = &c_array * &dx_beta;
let c_x_psi_beta = Some(Arc::new(c_x_psi_beta_dense.clone()));
let op = ImplicitHyperOperator {
implicit_deriv: Arc::clone(&implicit),
axis: 0,
x_design: Arc::clone(&x_design),
w_diag: crate::matrix::SignedWeightsArc::from_arc(Arc::clone(&w_diag)),
s_psi: s_psi.clone(),
p,
c_x_psi_beta,
};
let probes = [
array![1.0, 0.0],
array![0.0, 1.0],
array![0.7, -0.4],
array![-0.25, 1.10],
];
for (k, v) in probes.iter().enumerate() {
let xv = x_data.dot(v);
let dxv = dx_dpsi.dot(v);
let w_xv = &*w_diag * &xv;
let w_dxv = &*w_diag * &dxv;
let t1 = dx_dpsi.t().dot(&w_xv);
let t2 = x_data.t().dot(&w_dxv);
let weighted = &c_x_psi_beta_dense * &xv;
let t3 = x_data.t().dot(&weighted);
let t4 = s_psi.dot(v);
let want = &t1 + &t2 + &t3 + &t4;
let got = op.mul_vec(v);
assert_eq!(got.len(), p);
for i in 0..p {
let tol = 1e-12 * want[i].abs().max(1.0) + 1e-12;
assert!(
(want[i] - got[i]).abs() <= tol,
"B_d·v mismatch at probe {k}, comp {i}: want={:.6e}, got={:.6e}",
want[i],
got[i],
);
}
}
let op_gauss = ImplicitHyperOperator {
implicit_deriv: Arc::clone(&implicit),
axis: 0,
x_design,
w_diag: crate::matrix::SignedWeightsArc::from_arc(Arc::clone(&w_diag)),
s_psi: s_psi.clone(),
p,
c_x_psi_beta: None,
};
let v = array![0.7, -0.4];
let xv = x_data.dot(&v);
let dxv = dx_dpsi.dot(&v);
let w_xv = &*w_diag * &xv;
let w_dxv = &*w_diag * &dxv;
let want = &dx_dpsi.t().dot(&w_xv) + &x_data.t().dot(&w_dxv) + &s_psi.dot(&v);
let got = op_gauss.mul_vec(&v);
for i in 0..p {
let tol = 1e-12 * want[i].abs().max(1.0) + 1e-12;
assert!(
(want[i] - got[i]).abs() <= tol,
"Gaussian B_d·v mismatch at comp {i}: want={:.6e}, got={:.6e}",
want[i],
got[i],
);
}
}
#[test]
pub(crate) fn implicit_hyper_operator_third_derivative_term_centered_fd_matches_jacobian_column() {
use crate::terms::basis::ImplicitDesignPsiDerivative;
use std::sync::Arc;
let n = 5usize;
let n_knots = 3usize;
let n_axes = 1usize;
let p = n_knots;
let phi_values = Array1::from_vec((0..n * n_knots).map(|k| 0.1 + 0.05 * (k as f64)).collect());
let q_values = Array1::from_vec((0..n * n_knots).map(|k| -0.2 + 0.07 * (k as f64)).collect());
let t_values = Array1::zeros(n * n_knots);
let axis_components = Array2::from_shape_vec(
(n * n_knots, n_axes),
(0..n * n_knots).map(|k| 0.3 + 0.04 * (k as f64)).collect(),
)
.unwrap();
let implicit = Arc::new(ImplicitDesignPsiDerivative::new(
phi_values,
q_values,
t_values,
axis_components,
None,
None,
n,
n_knots,
0,
n_axes,
));
let x_data = array![
[1.0, 0.4, -0.2],
[0.5, 1.1, 0.3],
[-0.3, 0.9, 0.6],
[0.8, -0.5, 1.2],
[0.2, 0.7, -0.4],
];
let x_design = Arc::new(DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(
x_data.clone(),
)));
let w_diag = Arc::new(array![1.0, 0.7, 1.3, 0.9, 1.1]);
let s_psi = Array2::<f64>::eye(p) * 0.05;
let beta_eval = array![0.20, -0.10, 0.30];
let c_array = array![0.15, -0.08, 0.22, 0.05, -0.12];
let dx_dpsi = implicit.materialize_first(0).expect("materialize_first");
let dx_beta = dx_dpsi.dot(&beta_eval);
let c_x_psi_beta_dense = &c_array * &dx_beta;
let op = ImplicitHyperOperator {
implicit_deriv: Arc::clone(&implicit),
axis: 0,
x_design,
w_diag: crate::matrix::SignedWeightsArc::from_arc(w_diag),
s_psi,
p,
c_x_psi_beta: Some(Arc::new(c_x_psi_beta_dense.clone())),
};
let v_base = array![0.10, -0.05, 0.20];
let eps = 1e-6;
for j in 0..p {
let mut e_j = Array1::<f64>::zeros(p);
e_j[j] = 1.0;
let mut v_plus = v_base.clone();
v_plus[j] += eps;
let mut v_minus = v_base.clone();
v_minus[j] -= eps;
let fd = (&op.mul_vec(&v_plus) - &op.mul_vec(&v_minus)).mapv(|x| x / (2.0 * eps));
let analytic = op.mul_vec(&e_j);
for i in 0..p {
let tol = 1e-7 * analytic[i].abs().max(1.0) + 1e-7;
assert!(
(analytic[i] - fd[i]).abs() <= tol,
"FD col {j} mismatch at row {i}: analytic={:.6e}, fd={:.6e}",
analytic[i],
fd[i],
);
}
}
}
#[test]
pub(crate) fn test_pseudoinverse_scalar() {
let mut g = Array2::<f64>::zeros((1, 1));
g[[0, 0]] = 4.0;
let v = Array1::from_vec(vec![8.0]);
let result = pseudoinverse_times_vec(&g, v.as_slice().expect("contiguous test vector"), 1e-8);
assert!((result[0] - 2.0).abs() < 1e-12);
}
#[test]
pub(crate) fn corrected_covariance_indefinite_returns_diagnostic() {
let outer = ndarray::arr2(&[[2.0_f64, 0.0], [0.0, -1.0]]);
let base = Array2::<f64>::eye(2);
let hop = DenseSpectralOperator::from_symmetric(&base)
.expect("DenseSpectralOperator from identity should succeed");
let v0 = Array1::from_vec(vec![0.1, 0.2]);
let v1 = Array1::from_vec(vec![0.3, 0.4]);
let res = compute_corrected_covariance_with_constraints(
&[v0.clone(), v1.clone()],
&[],
&outer,
&hop,
None,
f64::NAN,
);
match res {
Err(CorrectedCovarianceError::Indefinite(diag)) => {
assert!(
diag.min_eigenvalue < -0.5,
"min eigenvalue should be ~-1, got {}",
diag.min_eigenvalue,
);
assert!(
diag.active_constraints.is_empty(),
"no theta supplied ⇒ no active constraints",
);
assert!(
!diag.suggested_action.is_empty(),
"diagnostic must include a suggested-action message",
);
}
Err(other) => panic!("expected Indefinite diagnostic, got error: {:?}", other),
Ok(cov) => panic!(
"indefinite outer Hessian must NOT yield a covariance; got matrix shape {:?}",
cov.matrix.shape(),
),
}
let res_legacy = compute_corrected_covariance(&[v0, v1], &[], &outer, &hop);
assert!(
matches!(res_legacy, Err(CorrectedCovarianceError::Indefinite(_))),
"legacy entry point must also surface Indefinite, got: {:?}",
res_legacy.map(|m| m.shape().to_vec()),
);
}
#[test]
pub(crate) fn corrected_covariance_indefinite_with_active_bound_succeeds() {
let outer = ndarray::arr2(&[[3.0_f64, 0.0], [0.0, -2.0]]);
let base = Array2::<f64>::eye(2);
let hop = DenseSpectralOperator::from_symmetric(&base).expect("hop");
let v0 = Array1::from_vec(vec![0.5, 0.0]);
let v1 = Array1::from_vec(vec![0.0, 0.5]);
let theta = vec![0.0_f64, crate::solver::estimate::RHO_BOUND];
let res = compute_corrected_covariance_with_constraints(
&[v0, v1],
&[],
&outer,
&hop,
Some(&theta),
0.0,
)
.expect("free-subspace SPD ⇒ covariance returned");
assert_eq!(res.active_constraints, vec![1]);
assert!(res.matrix.iter().all(|v| v.is_finite()));
}
pub(crate) fn build_leak_proof_solution(
rho: &[f64],
x: &Array2<f64>,
s1: &Array2<f64>,
s2: &Array2<f64>,
xty: &Array1<f64>,
yty: f64,
c_array: Array1<f64>,
use_projected_kernel: bool,
) -> InnerSolution<'static> {
let p = x.ncols();
let n = x.nrows();
assert_eq!(rho.len(), 2);
let lambdas: Vec<f64> = rho.iter().map(|r| r.exp()).collect();
let xtx = crate::faer_ndarray::fast_atb(x, x);
let mut s_lambda = Array2::<f64>::zeros((p, p));
s_lambda.scaled_add(lambdas[0], s1);
s_lambda.scaled_add(lambdas[1], s2);
let mut h = xtx.clone();
h += &s_lambda;
let hop = DenseSpectralOperator::from_symmetric(&h).unwrap();
let beta = hop.solve(xty);
let deviance = yty - 2.0 * beta.dot(xty) + beta.dot(&xtx.dot(&beta));
let log_lik = -0.5 * deviance;
let penalty_quad = beta.dot(&s_lambda.dot(&beta));
let (s_eigs, _) = s_lambda.eigh(faer::Side::Lower).unwrap();
let threshold = positive_eigenvalue_threshold(s_eigs.as_slice().unwrap());
let log_det_s = exact_pseudo_logdet(s_eigs.as_slice().unwrap(), threshold);
let eps_det = 1e-7;
let mut det1 = Array1::zeros(2);
for k in 0..2 {
let mut rp = rho.to_vec();
rp[k] += eps_det;
let lp: Vec<f64> = rp.iter().map(|r| r.exp()).collect();
let mut sp = Array2::<f64>::zeros((p, p));
sp.scaled_add(lp[0], s1);
sp.scaled_add(lp[1], s2);
let (ev_p, _) = sp.eigh(faer::Side::Lower).unwrap();
let thp = positive_eigenvalue_threshold(ev_p.as_slice().unwrap());
let ld_p = exact_pseudo_logdet(ev_p.as_slice().unwrap(), thp);
let mut rm = rho.to_vec();
rm[k] -= eps_det;
let lm: Vec<f64> = rm.iter().map(|r| r.exp()).collect();
let mut sm = Array2::<f64>::zeros((p, p));
sm.scaled_add(lm[0], s1);
sm.scaled_add(lm[1], s2);
let (ev_m, _) = sm.eigh(faer::Side::Lower).unwrap();
let thm = positive_eigenvalue_threshold(ev_m.as_slice().unwrap());
let ld_m = exact_pseudo_logdet(ev_m.as_slice().unwrap(), thm);
det1[k] = (ld_p - ld_m) / (2.0 * eps_det);
}
let (s_full_eigs, s_full_vecs) = s_lambda.eigh(faer::Side::Lower).unwrap();
let s_thresh = positive_eigenvalue_threshold(s_full_eigs.as_slice().unwrap());
let active: Vec<usize> = s_full_eigs
.iter()
.enumerate()
.filter(|(_, v)| **v > s_thresh)
.map(|(i, _)| i)
.collect();
let r_rank = active.len();
let mut u_s = Array2::<f64>::zeros((p, r_rank));
for (j, &idx) in active.iter().enumerate() {
for i in 0..p {
u_s[[i, j]] = s_full_vecs[[i, idx]];
}
}
let h_proj = u_s.t().dot(&h).dot(&u_s);
let (hp_eigs, hp_vecs) = h_proj.eigh(faer::Side::Lower).unwrap();
let mut h_proj_inv = Array2::<f64>::zeros((r_rank, r_rank));
for i in 0..r_rank {
for j in 0..r_rank {
let mut acc = 0.0;
for k_idx in 0..r_rank {
acc += hp_vecs[[i, k_idx]] * hp_vecs[[j, k_idx]] / hp_eigs[k_idx];
}
h_proj_inv[[i, j]] = acc;
}
}
let log_det_h_proj: f64 = hp_eigs.iter().map(|v| v.ln()).sum();
let log_det_h_full = hop.logdet();
let hessian_logdet_correction = log_det_h_proj - log_det_h_full;
let deriv_provider = SinglePredictorGlmDerivatives {
c_array,
d_array: None,
hessian_weights: Array1::ones(n),
x_transformed: DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x.clone())),
};
let r1 = penalty_matrix_root(s1).unwrap();
let r2 = penalty_matrix_root(s2).unwrap();
let penalty_subspace_trace = if use_projected_kernel {
Some(Arc::new(PenaltySubspaceTrace {
u_s,
h_proj_inverse: h_proj_inv,
}))
} else {
None
};
InnerSolution {
log_likelihood: log_lik,
penalty_quadratic: penalty_quad,
hessian_op: Arc::new(hop),
beta,
penalty_coords: vec![
PenaltyCoordinate::from_dense_root(r1),
PenaltyCoordinate::from_dense_root(r2),
],
penalty_logdet: PenaltyLogdetDerivs {
value: log_det_s,
first: det1,
second: None,
},
deriv_provider: Box::new(deriv_provider),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction,
penalty_subspace_trace,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: n,
nullspace_dim: (p - r_rank) as f64,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
},
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
}
}
#[test]
pub(crate) fn proof_outer_rho_projected_kernel_fixes_leak() {
let n = 100;
let p = 3;
let mut x = Array2::<f64>::zeros((n, p));
for i in 0..n {
let t = (i as f64) / ((n - 1) as f64);
x[[i, 0]] = 1.0; x[[i, 1]] = (2.0 * std::f64::consts::PI * t).sin();
x[[i, 2]] = (t - 0.5) * (t - 0.3);
}
let mut s1 = Array2::<f64>::zeros((p, p));
s1[[1, 1]] = 1.0;
let mut s2 = Array2::<f64>::zeros((p, p));
s2[[2, 2]] = 1.0;
let mut y = Array1::<f64>::zeros(n);
for i in 0..n {
let t = (i as f64) / ((n - 1) as f64);
y[i] = 0.7 + 0.4 * (2.0 * std::f64::consts::PI * t).sin() + 0.1 * ((i as f64) * 0.7).cos();
}
let xty = crate::faer_ndarray::fast_atb(&x, &y.clone().insert_axis(ndarray::Axis(1)))
.column(0)
.to_owned();
let yty = y.dot(&y);
let c_array = Array1::from_shape_fn(n, |i| {
0.3 + 0.5 * ((i as f64) * 0.11).sin() + 0.2 * ((i as f64) * 0.27).cos()
});
let rho = vec![0.0_f64, 12.0_f64];
let delta = 1e-4;
let mut fd_grad = [0.0_f64; 2];
for j in 0..2 {
let mut rp = rho.clone();
rp[j] += delta;
let sp = build_leak_proof_solution(&rp, &x, &s1, &s2, &xty, yty, c_array.clone(), true);
let cost_p = reml_laml_evaluate(&sp, &rp, EvalMode::ValueOnly, None)
.unwrap()
.cost;
let mut rm = rho.clone();
rm[j] -= delta;
let sm = build_leak_proof_solution(&rm, &x, &s1, &s2, &xty, yty, c_array.clone(), true);
let cost_m = reml_laml_evaluate(&sm, &rm, EvalMode::ValueOnly, None)
.unwrap()
.cost;
fd_grad[j] = (cost_p - cost_m) / (2.0 * delta);
}
let sol_unproj =
build_leak_proof_solution(&rho, &x, &s1, &s2, &xty, yty, c_array.clone(), false);
let g_unproj = reml_laml_evaluate(&sol_unproj, &rho, EvalMode::ValueAndGradient, None)
.unwrap()
.gradient
.unwrap();
let sol_proj = build_leak_proof_solution(&rho, &x, &s1, &s2, &xty, yty, c_array.clone(), true);
let g_proj = reml_laml_evaluate(&sol_proj, &rho, EvalMode::ValueAndGradient, None)
.unwrap()
.gradient
.unwrap();
eprintln!(
"=== Outer-ρ gradient at runaway ρ = {:?} (λ = {:?}) ===",
rho,
rho.iter().map(|r| r.exp()).collect::<Vec<_>>()
);
for j in 0..2 {
eprintln!(
" coord {}: FD={:+.6e} unprojected_analytic={:+.6e} projected_analytic={:+.6e}",
j, fd_grad[j], g_unproj[j], g_proj[j]
);
let rel_proj = (g_proj[j] - fd_grad[j]).abs() / fd_grad[j].abs().max(1e-12);
let rel_unproj = (g_unproj[j] - fd_grad[j]).abs() / fd_grad[j].abs().max(1e-12);
eprintln!(
" |projected − FD|/|FD| = {:.3e} |unprojected − FD|/|FD| = {:.3e}",
rel_proj, rel_unproj
);
}
eprintln!("=== Sweep λ_2 (coord 1) — unprojected analytic vs FD ===");
for &rho2 in &[6.0_f64, 9.0, 12.0, 15.0, 18.0, 20.0] {
let r = vec![0.0_f64, rho2];
let fd1 = {
let mut rp = r.clone();
rp[1] += delta;
let sp = build_leak_proof_solution(&rp, &x, &s1, &s2, &xty, yty, c_array.clone(), true);
let cp = reml_laml_evaluate(&sp, &rp, EvalMode::ValueOnly, None)
.unwrap()
.cost;
let mut rm = r.clone();
rm[1] -= delta;
let sm = build_leak_proof_solution(&rm, &x, &s1, &s2, &xty, yty, c_array.clone(), true);
let cm = reml_laml_evaluate(&sm, &rm, EvalMode::ValueOnly, None)
.unwrap()
.cost;
(cp - cm) / (2.0 * delta)
};
let su = build_leak_proof_solution(&r, &x, &s1, &s2, &xty, yty, c_array.clone(), false);
let gu = reml_laml_evaluate(&su, &r, EvalMode::ValueAndGradient, None)
.unwrap()
.gradient
.unwrap();
let sp = build_leak_proof_solution(&r, &x, &s1, &s2, &xty, yty, c_array.clone(), true);
let gp = reml_laml_evaluate(&sp, &r, EvalMode::ValueAndGradient, None)
.unwrap()
.gradient
.unwrap();
let leak = gu[1] - fd1;
eprintln!(
" ρ_2={:5.1} λ_2={:+.3e} FD={:+.6e} unproj={:+.6e} proj={:+.6e} leak(unproj−FD)={:+.6e}",
rho2,
rho2.exp(),
fd1,
gu[1],
gp[1],
leak
);
}
let j = 1;
let rel_proj = (g_proj[j] - fd_grad[j]).abs() / fd_grad[j].abs().max(1e-12);
let rel_unproj = (g_unproj[j] - fd_grad[j]).abs() / fd_grad[j].abs().max(1e-12);
assert!(
rel_proj < 1e-2,
"projected gradient should match FD at runaway coord: \
FD={:+.6e}, projected={:+.6e}, rel={:.3e}",
fd_grad[j],
g_proj[j],
rel_proj
);
assert!(
rel_unproj > 0.5,
"unprojected gradient should DISAGREE with FD at runaway coord: \
FD={:+.6e}, unprojected={:+.6e}, rel={:.3e}",
fd_grad[j],
g_unproj[j],
rel_unproj
);
}
pub(crate) fn build_gaussian_solution_at_beta(
rho: &[f64],
beta_hat: Array1<f64>,
attach_residual: bool,
) -> InnerSolution<'_> {
let p = 3usize;
let n = 50usize;
let xtx = array![[10.0, 2.0, 1.0], [2.0, 8.0, 0.5], [1.0, 0.5, 6.0]];
let s1 = array![[1.0, 0.2, 0.0], [0.2, 1.0, 0.0], [0.0, 0.0, 0.0]];
let s2 = array![[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
let xty = array![5.0, 3.0, 2.0];
let yty = 20.0;
let lambdas: Vec<f64> = rho.iter().map(|&r| r.exp()).collect();
let mut h = xtx.clone();
h.scaled_add(lambdas[0], &s1);
h.scaled_add(lambdas[1], &s2);
let op = DenseSpectralOperator::from_symmetric(&h).unwrap();
let penalty_quad = lambdas[0] * beta_hat.dot(&s1.dot(&beta_hat))
+ lambdas[1] * beta_hat.dot(&s2.dot(&beta_hat));
let deviance = yty - 2.0 * beta_hat.dot(&xty) + beta_hat.dot(&xtx.dot(&beta_hat));
let log_likelihood = -0.5 * deviance;
let kkt_residual = if attach_residual {
Some(ProjectedKktResidual::from_active_projected(
&h.dot(&beta_hat) - &xty,
))
} else {
None
};
let r1 = penalty_matrix_root(&s1).unwrap();
let r2 = penalty_matrix_root(&s2).unwrap();
let penalty_logdet = gaussian_penalty_logdet_fd(p, &s1, &s2, rho);
InnerSolution {
log_likelihood,
penalty_quadratic: penalty_quad,
hessian_op: Arc::new(op),
beta: beta_hat,
penalty_coords: vec![
PenaltyCoordinate::from_dense_root(r1),
PenaltyCoordinate::from_dense_root(r2),
],
penalty_logdet,
deriv_provider: Box::new(GaussianDerivatives),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction: 0.0,
penalty_subspace_trace: None,
rho_curvature_scale: 1.0,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: n,
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::ProfiledGaussian,
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
}
}
#[test]
pub(crate) fn malformed_projected_kkt_residual_is_contract_error() {
let rho: Vec<f64> = vec![1.0, -0.5];
let beta_hat = array![0.1, -0.2, 0.3];
let mut sol = build_gaussian_solution_at_beta(&rho, beta_hat, false);
sol.dispersion = DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
};
sol.kkt_residual = Some(ProjectedKktResidual::from_active_projected(array![
0.0, 0.0
]));
let err = match reml_laml_evaluate(&sol, &rho, EvalMode::ValueAndGradient, None) {
Ok(_) => panic!("wrong-length projected KKT residual must be rejected"),
Err(err) => err,
};
assert!(
err.contains("projected KKT residual length mismatch"),
"unexpected error: {err}"
);
}
#[test]
pub(crate) fn ift_correction_vanishes_at_exact_kkt() {
let rho: Vec<f64> = vec![1.0, -0.5];
let xtx = array![[10.0, 2.0, 1.0], [2.0, 8.0, 0.5], [1.0, 0.5, 6.0]];
let s1 = array![[1.0, 0.2, 0.0], [0.2, 1.0, 0.0], [0.0, 0.0, 0.0]];
let s2 = array![[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
let xty = array![5.0, 3.0, 2.0];
let lambdas: Vec<f64> = rho.iter().map(|&r| r.exp()).collect();
let mut h = xtx.clone();
h.scaled_add(lambdas[0], &s1);
h.scaled_add(lambdas[1], &s2);
let op_for_solve = DenseSpectralOperator::from_symmetric(&h).unwrap();
let beta_star = op_for_solve.solve(&xty);
let sol_envelope = build_gaussian_solution_at_beta(&rho, beta_star.clone(), false);
let grad_envelope = reml_laml_evaluate(&sol_envelope, &rho, EvalMode::ValueAndGradient, None)
.unwrap()
.gradient
.unwrap();
let cost_envelope = reml_laml_evaluate(&sol_envelope, &rho, EvalMode::ValueOnly, None)
.unwrap()
.cost;
let sol_with_residual = build_gaussian_solution_at_beta(&rho, beta_star.clone(), true);
let r_norm = sol_with_residual
.kkt_residual
.as_ref()
.unwrap()
.as_array()
.iter()
.fold(0.0_f64, |acc, &v| acc.max(v.abs()));
assert!(
r_norm < 1e-10,
"residual at exact β* should be numerically zero, got ‖r‖∞ = {:.3e}",
r_norm
);
let result_ift =
reml_laml_evaluate(&sol_with_residual, &rho, EvalMode::ValueAndGradient, None).unwrap();
let grad_ift = result_ift.gradient.unwrap();
let cost_ift = result_ift.cost;
assert_relative_eq!(
cost_ift,
cost_envelope,
epsilon = 1e-10,
max_relative = 1e-10
);
for k in 0..rho.len() {
assert_relative_eq!(
grad_ift[k],
grad_envelope[k],
epsilon = 1e-10,
max_relative = 1e-8
);
}
}
#[test]
pub(crate) fn rho_gradient_at_upper_bound_is_zero_envelope_and_ift_consistent_issue_197() {
let rho: Vec<f64> = vec![crate::solver::estimate::RHO_BOUND, -0.5];
let beta_hat = array![0.7, -0.4, 0.2];
let sol_envelope = build_gaussian_solution_at_beta(&rho, beta_hat.clone(), false);
let result_env =
reml_laml_evaluate(&sol_envelope, &rho, EvalMode::ValueAndGradient, None).unwrap();
let grad_env = result_env.gradient.unwrap();
assert_eq!(
grad_env[0], 0.0,
"envelope ρ-gradient at active upper bound must be exactly 0.0 \
(gradient-projection convention, see #197); got {:+.6e}",
grad_env[0]
);
let sol_with_residual = build_gaussian_solution_at_beta(&rho, beta_hat, true);
let result_ift =
reml_laml_evaluate(&sol_with_residual, &rho, EvalMode::ValueAndGradient, None).unwrap();
let grad_ift = result_ift.gradient.unwrap();
assert_eq!(
grad_ift[0], 0.0,
"IFT-corrected ρ-gradient at active upper bound must be exactly 0.0 \
— envelope and IFT-correction blocks must agree (#197); got {:+.6e}",
grad_ift[0]
);
assert!(
grad_env[1].abs() > 1e-8,
"free-coord envelope gradient should be non-trivial: got {:+.6e}",
grad_env[1]
);
assert!(
grad_ift[1].abs() > 1e-8,
"free-coord IFT-corrected gradient should be non-trivial: got {:+.6e}",
grad_ift[1]
);
}
#[test]
pub(crate) fn ift_correction_recovers_fd_at_perturbed_beta() {
let rho: Vec<f64> = vec![0.5, 0.3];
let xtx = array![[10.0, 2.0, 1.0], [2.0, 8.0, 0.5], [1.0, 0.5, 6.0]];
let s1 = array![[1.0, 0.2, 0.0], [0.2, 1.0, 0.0], [0.0, 0.0, 0.0]];
let s2 = array![[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
let xty = array![5.0, 3.0, 2.0];
let lambdas: Vec<f64> = rho.iter().map(|&r| r.exp()).collect();
let mut h = xtx.clone();
h.scaled_add(lambdas[0], &s1);
h.scaled_add(lambdas[1], &s2);
let op_for_solve = DenseSpectralOperator::from_symmetric(&h).unwrap();
let beta_star = op_for_solve.solve(&xty);
fn to_fixed<'a>(mut sol: InnerSolution<'a>) -> InnerSolution<'a> {
sol.dispersion = DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
};
sol
}
let fd_eps = 1e-5;
let mut fd_grad = Array1::<f64>::zeros(rho.len());
for k in 0..rho.len() {
let mut rho_plus = rho.clone();
rho_plus[k] += fd_eps;
let mut h_plus = xtx.clone();
let lambdas_plus: Vec<f64> = rho_plus.iter().map(|&r| r.exp()).collect();
h_plus.scaled_add(lambdas_plus[0], &s1);
h_plus.scaled_add(lambdas_plus[1], &s2);
let beta_star_plus = DenseSpectralOperator::from_symmetric(&h_plus)
.unwrap()
.solve(&xty);
let sol_plus = to_fixed(build_gaussian_solution_at_beta(
&rho_plus,
beta_star_plus,
false,
));
let cost_plus = reml_laml_evaluate(&sol_plus, &rho_plus, EvalMode::ValueOnly, None)
.unwrap()
.cost;
let mut rho_minus = rho.clone();
rho_minus[k] -= fd_eps;
let mut h_minus = xtx.clone();
let lambdas_minus: Vec<f64> = rho_minus.iter().map(|&r| r.exp()).collect();
h_minus.scaled_add(lambdas_minus[0], &s1);
h_minus.scaled_add(lambdas_minus[1], &s2);
let beta_star_minus = DenseSpectralOperator::from_symmetric(&h_minus)
.unwrap()
.solve(&xty);
let sol_minus = to_fixed(build_gaussian_solution_at_beta(
&rho_minus,
beta_star_minus,
false,
));
let cost_minus = reml_laml_evaluate(&sol_minus, &rho_minus, EvalMode::ValueOnly, None)
.unwrap()
.cost;
fd_grad[k] = (cost_plus - cost_minus) / (2.0 * fd_eps);
}
let perturb = Array1::from_vec(vec![0.02, -0.015, 0.025]);
let beta_hat = &beta_star + &perturb;
let sol_envelope = to_fixed(build_gaussian_solution_at_beta(
&rho,
beta_hat.clone(),
false,
));
let grad_envelope = reml_laml_evaluate(&sol_envelope, &rho, EvalMode::ValueAndGradient, None)
.unwrap()
.gradient
.unwrap();
let sol_ift = to_fixed(build_gaussian_solution_at_beta(
&rho,
beta_hat.clone(),
true,
));
let r_norm = sol_ift
.kkt_residual
.as_ref()
.unwrap()
.as_array()
.iter()
.fold(0.0_f64, |acc, &v| acc.max(v.abs()));
assert!(
r_norm > 1e-3,
"perturbed β̂ should produce a non-trivial residual, got ‖r‖∞ = {:.3e}",
r_norm
);
let grad_ift = reml_laml_evaluate(&sol_ift, &rho, EvalMode::ValueAndGradient, None)
.unwrap()
.gradient
.unwrap();
let mut at_least_one_improved = false;
for k in 0..rho.len() {
let err_envelope = (grad_envelope[k] - fd_grad[k]).abs();
let err_ift = (grad_ift[k] - fd_grad[k]).abs();
assert!(
err_ift <= err_envelope * 1.05 + 1e-9,
"IFT correction must not enlarge gradient error: coord={} envelope_err={:.3e} \
ift_err={:.3e} FD={:.6e}",
k,
err_envelope,
err_ift,
fd_grad[k]
);
if err_ift < err_envelope * 0.5 && err_envelope > 1e-6 {
at_least_one_improved = true;
}
}
assert!(
at_least_one_improved,
"IFT correction should improve gradient accuracy on at least one coord: \
envelope=[{:.3e}, {:.3e}] ift=[{:.3e}, {:.3e}] fd=[{:.3e}, {:.3e}]",
(grad_envelope[0] - fd_grad[0]).abs(),
(grad_envelope[1] - fd_grad[1]).abs(),
(grad_ift[0] - fd_grad[0]).abs(),
(grad_ift[1] - fd_grad[1]).abs(),
fd_grad[0],
fd_grad[1],
);
}
#[test]
pub(crate) fn ift_correction_recovers_fd_hessian_at_perturbed_beta() {
let rho: Vec<f64> = vec![0.5, 0.3];
let xtx = array![[10.0, 2.0, 1.0], [2.0, 8.0, 0.5], [1.0, 0.5, 6.0]];
let s1 = array![[1.0, 0.2, 0.0], [0.2, 1.0, 0.0], [0.0, 0.0, 0.0]];
let s2 = array![[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
let xty = array![5.0, 3.0, 2.0];
fn to_fixed<'a>(mut sol: InnerSolution<'a>) -> InnerSolution<'a> {
sol.dispersion = DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: true,
};
sol
}
let solve_beta_star = |rho_eval: &[f64]| -> Array1<f64> {
let lambdas_eval: Vec<f64> = rho_eval.iter().map(|&r| r.exp()).collect();
let mut h = xtx.clone();
h.scaled_add(lambdas_eval[0], &s1);
h.scaled_add(lambdas_eval[1], &s2);
DenseSpectralOperator::from_symmetric(&h)
.unwrap()
.solve(&xty)
};
let exact_profile_cost = |rho_eval: &[f64]| -> f64 {
let beta_star = solve_beta_star(rho_eval);
let sol = to_fixed(build_gaussian_solution_at_beta(rho_eval, beta_star, false));
reml_laml_evaluate(&sol, rho_eval, EvalMode::ValueOnly, None)
.unwrap()
.cost
};
let fd_eps = 2e-4;
let mut fd_hessian = Array2::<f64>::zeros((rho.len(), rho.len()));
let center_cost = exact_profile_cost(&rho);
for i in 0..rho.len() {
for j in i..rho.len() {
let value = if i == j {
let mut rho_plus = rho.clone();
rho_plus[i] += fd_eps;
let mut rho_minus = rho.clone();
rho_minus[i] -= fd_eps;
(exact_profile_cost(&rho_plus) - 2.0 * center_cost + exact_profile_cost(&rho_minus))
/ (fd_eps * fd_eps)
} else {
let mut pp = rho.clone();
pp[i] += fd_eps;
pp[j] += fd_eps;
let mut pm = rho.clone();
pm[i] += fd_eps;
pm[j] -= fd_eps;
let mut mp = rho.clone();
mp[i] -= fd_eps;
mp[j] += fd_eps;
let mut mm = rho.clone();
mm[i] -= fd_eps;
mm[j] -= fd_eps;
(exact_profile_cost(&pp) - exact_profile_cost(&pm) - exact_profile_cost(&mp)
+ exact_profile_cost(&mm))
/ (4.0 * fd_eps * fd_eps)
};
fd_hessian[[i, j]] = value;
if i != j {
fd_hessian[[j, i]] = value;
}
}
}
let beta_star = solve_beta_star(&rho);
let beta_hat = &beta_star + &Array1::from_vec(vec![0.02, -0.015, 0.025]);
let sol_envelope = to_fixed(build_gaussian_solution_at_beta(
&rho,
beta_hat.clone(),
false,
));
let hessian_envelope =
reml_laml_evaluate(&sol_envelope, &rho, EvalMode::ValueGradientHessian, None)
.unwrap()
.hessian
.unwrap_analytic();
let sol_ift = to_fixed(build_gaussian_solution_at_beta(&rho, beta_hat, true));
let hessian_ift = reml_laml_evaluate(&sol_ift, &rho, EvalMode::ValueGradientHessian, None)
.unwrap()
.hessian
.unwrap_analytic();
let mut envelope_was_wrong = false;
for i in 0..rho.len() {
for j in 0..rho.len() {
let envelope_err = (hessian_envelope[[i, j]] - fd_hessian[[i, j]]).abs();
let ift_err = (hessian_ift[[i, j]] - fd_hessian[[i, j]]).abs();
assert!(
ift_err <= envelope_err * 0.25 + 2e-5,
"IFT Hessian correction failed at ({}, {}): envelope={:.8e} ift={:.8e} \
fd={:.8e} envelope_err={:.3e} ift_err={:.3e}",
i,
j,
hessian_envelope[[i, j]],
hessian_ift[[i, j]],
fd_hessian[[i, j]],
envelope_err,
ift_err
);
if envelope_err > 1e-4 && ift_err < envelope_err * 0.1 {
envelope_was_wrong = true;
}
}
}
assert!(
envelope_was_wrong,
"test did not reproduce the Hessian bug: envelope={:?} ift={:?} fd={:?}",
hessian_envelope, hessian_ift, fd_hessian
);
}
#[test]
pub(crate) fn bug_hunt_penalty_matrix_root_reconstructs_with_effective_rank() {
let s = ndarray::arr2(&[
[2.0_f64, 1.0, 0.0, 0.0],
[1.0, 2.0, 0.0, 0.0],
[0.0, 0.0, 1e-14, 0.0],
[0.0, 0.0, 0.0, 0.0],
]);
let l = penalty_matrix_root(&s)
.expect("penalty matrix root should be computable for semidefinite inputs");
let recon = l.t().dot(&l);
let frob_s = s.iter().map(|x| x * x).sum::<f64>().sqrt();
let frob_err = (&recon - &s).iter().map(|x| x * x).sum::<f64>().sqrt();
let rel = frob_err / frob_s.max(1.0);
assert!(
rel < 1e-9,
"Penalty root must reconstruct S_lambda (as RᵀR) to numerical tolerance, but relative Frobenius error was {rel:.3e}.",
);
}
#[test]
pub(crate) fn bug_hunt_block_penalty_logdet_derivs_match_finite_difference_shared_columns() {
let s1 = ndarray::arr2(&[[2.0_f64, 1.0], [1.0, 1.0]]);
let s2 = ndarray::arr2(&[[1.0_f64, 0.5], [0.5, 2.0]]);
let rho = ndarray::arr1(&[0.2_f64, -0.4]);
let delta = 1e-6_f64;
let per_block_rho = vec![rho.clone()];
let penalties_block = vec![s1.clone(), s2.clone()];
let per_block_penalties: Vec<&[Array2<f64>]> = vec![penalties_block.as_slice()];
let out = compute_block_penalty_logdet_derivs(&per_block_rho, &per_block_penalties, delta)
.expect("logdet derivs should be finite");
let f = |r: &Array1<f64>| -> f64 {
let l1 = r[0].exp();
let l2 = r[1].exp();
let s = s1.mapv(|x| x * l1) + s2.mapv(|x| x * l2) + Array2::<f64>::eye(2) * delta;
let op = DenseSpectralOperator::from_symmetric(&s).expect("spd for fd");
op.logdet()
};
let eps = 1e-5_f64;
for k in 0..2 {
let mut rp = rho.clone();
rp[k] += eps;
let mut rm = rho.clone();
rm[k] -= eps;
let fd = (f(&rp) - f(&rm)) / (2.0 * eps);
assert!(
(out.first[k] - fd).abs() < 1e-5,
"First derivative with shared penalty columns must match finite differences; coord {k} analytic={:.8e} finite_diff={:.8e}.",
out.first[k],
fd
);
}
}
pub(crate) fn build_scaled_curvature_solution(rho: &[f64], s: f64) -> InnerSolution<'static> {
let h_unp = array![[3.0_f64, 0.5], [0.5, 5.0]];
let s_root = Array2::<f64>::eye(2);
let penalty_coord = PenaltyCoordinate::from_dense_root(s_root.clone());
let s_mat = s_root.dot(&s_root.t());
assert_eq!(rho.len(), 1, "single-ρ scaled-curvature test");
let lambda = rho[0].exp();
let mut h_op_dense = &h_unp + &(&s_mat * lambda);
h_op_dense.mapv_inplace(|v| s * v);
let hop =
Arc::new(DenseSpectralOperator::from_symmetric(&h_op_dense).expect("scaled H_op is SPD"));
let p = h_op_dense.nrows() as f64;
let hessian_logdet_correction = -p * s.ln();
InnerSolution {
log_likelihood: 0.0,
penalty_quadratic: 0.0,
hessian_op: hop,
beta: array![0.0_f64, 0.0],
penalty_coords: vec![penalty_coord],
penalty_logdet: PenaltyLogdetDerivs {
value: 0.0,
first: array![2.0],
second: Some(array![[0.0]]),
},
deriv_provider: Box::new(GaussianDerivatives),
tk_correction: 0.0,
tk_gradient: None,
firth: None,
hessian_logdet_correction,
penalty_subspace_trace: None,
rho_curvature_scale: s,
rho_prior: crate::types::RhoPrior::Flat,
n_observations: 10,
nullspace_dim: 0.0,
gaussian_weight_log_sum_half: 0.0,
dispersion: DispersionHandling::Fixed {
phi: 1.0,
include_logdet_h: true,
include_logdet_s: false,
},
ext_coords: Vec::new(),
ext_coord_pair_fn: None,
rho_ext_pair_fn: None,
fixed_drift_deriv: None,
contracted_psi_second_order: None,
barrier_config: None,
kkt_residual: None,
active_constraints: None,
stochastic_trace_state: Arc::new(Mutex::new(StochasticTraceState::default())),
}
}
#[test]
pub(crate) fn issue_200_cost_gradient_agree_under_rho_curvature_scale() {
let s = 2.0_f64;
let rho0 = vec![0.3_f64];
let solution_center = build_scaled_curvature_solution(&rho0, s);
let result = reml_laml_evaluate(&solution_center, &rho0, EvalMode::ValueAndGradient, None)
.expect("center evaluation");
let analytic = result
.gradient
.expect("gradient returned for fixed-dispersion path")[0];
let eps = 1e-6_f64;
let mut rho_plus = rho0.clone();
rho_plus[0] += eps;
let mut rho_minus = rho0.clone();
rho_minus[0] -= eps;
let cost_plus = reml_laml_evaluate(
&build_scaled_curvature_solution(&rho_plus, s),
&rho_plus,
EvalMode::ValueOnly,
None,
)
.expect("forward evaluation")
.cost;
let cost_minus = reml_laml_evaluate(
&build_scaled_curvature_solution(&rho_minus, s),
&rho_minus,
EvalMode::ValueOnly,
None,
)
.expect("backward evaluation")
.cost;
let fd = (cost_plus - cost_minus) / (2.0 * eps);
assert!(
(analytic - fd).abs() < 1e-6,
"issue #200: cost/gradient must agree under rho_curvature_scale={s} \
(analytic={analytic:.10e}, fd={fd:.10e}, |diff|={:.3e})",
(analytic - fd).abs(),
);
}
#[test]
pub(crate) fn issue_200_rejects_non_positive_rho_curvature_scale() {
let mut solution = build_scaled_curvature_solution(&[0.0_f64], 1.0);
solution.rho_curvature_scale = 0.0;
let err = reml_laml_evaluate(&solution, &[0.0_f64], EvalMode::ValueOnly, None)
.expect_err("zero curvature scale must be rejected");
assert!(
format!("{err}").contains("rho_curvature_scale"),
"error message must name the offending field, got: {err}",
);
let mut solution = build_scaled_curvature_solution(&[0.0_f64], 1.0);
solution.rho_curvature_scale = -1.5;
let err = reml_laml_evaluate(&solution, &[0.0_f64], EvalMode::ValueOnly, None)
.expect_err("negative curvature scale must be rejected");
assert!(
format!("{err}").contains("rho_curvature_scale"),
"error message must name the offending field, got: {err}",
);
let mut solution = build_scaled_curvature_solution(&[0.0_f64], 1.0);
solution.rho_curvature_scale = f64::NAN;
let err = reml_laml_evaluate(&solution, &[0.0_f64], EvalMode::ValueOnly, None)
.expect_err("NaN curvature scale must be rejected");
assert!(
format!("{err}").contains("rho_curvature_scale"),
"error message must name the offending field, got: {err}",
);
}
#[test]
pub(crate) fn dense_cholesky_value_only_matches_spectral() {
use approx::assert_relative_eq;
let h = array![
[6.0, 2.0, 1.0, 0.5],
[2.0, 5.0, 1.5, 0.25],
[1.0, 1.5, 4.0, 0.75],
[0.5, 0.25, 0.75, 3.0],
];
let spectral = DenseSpectralOperator::from_symmetric(&h).unwrap();
let cholesky = DenseCholeskyValueOnlyOperator::from_spd(&h).unwrap();
assert_relative_eq!(cholesky.logdet(), spectral.logdet(), epsilon = 1e-10);
assert_eq!(cholesky.dim(), 4);
assert_eq!(cholesky.active_rank(), 4);
let rhs = array![1.0, 2.0, 3.0, 4.0];
let sol_spec = HessianOperator::solve(&spectral, &rhs);
let sol_chol = HessianOperator::solve(&cholesky, &rhs);
for i in 0..4 {
assert_relative_eq!(sol_chol[i], sol_spec[i], epsilon = 1e-10);
}
let a = array![
[2.0, 1.0, 0.0, 0.0],
[1.0, 3.0, 0.5, 0.0],
[0.0, 0.5, 2.5, 1.0],
[0.0, 0.0, 1.0, 1.5],
];
assert_relative_eq!(
cholesky.trace_hinv_product(&a),
spectral.trace_hinv_product(&a),
epsilon = 1e-10
);
}