use crate::linalg::faer_ndarray::FaerEigh;
use crate::linalg::lanczos::{SymmetricLanczosOptions, symmetric_lanczos_eigenpairs};
use faer::Side;
use ndarray::{Array1, Array2, ArrayView2};
#[inline]
fn norm2_slice(a: &[f64]) -> f64 {
a.iter().map(|x| x * x).sum::<f64>().sqrt()
}
const REDUCED_INFO_RELATIVE_FLOOR: f64 = 1e-10;
const REDUCED_INFO_ABSOLUTE_FLOOR: f64 = 1e-12;
const CONDITIONING_GATE_RELATIVE: f64 = 1e-8;
const CONDITIONING_GATE_ABSOLUTE: f64 = 1.0;
const CONDITIONING_GATE_ABSOLUTE_CLEAR: f64 = 16.0;
const CONDITIONING_GATE_RELATIVE_CLEAR: f64 = 1e-6;
#[inline]
fn conditioning_gate_weight(lambda_min: f64, lambda_max: f64) -> f64 {
if lambda_max <= 0.0 {
return 1.0;
}
if !lambda_min.is_finite() {
return 1.0;
}
#[inline]
fn ramp_down(x: f64, under: f64, clear: f64) -> f64 {
if x <= under {
return 1.0;
}
if x >= clear {
return 0.0;
}
let t = (x - under) / (clear - under);
1.0 - t * t * (3.0 - 2.0 * t)
}
let w_abs = ramp_down(
lambda_min,
CONDITIONING_GATE_ABSOLUTE,
CONDITIONING_GATE_ABSOLUTE_CLEAR,
);
let ratio = (lambda_min / lambda_max).max(f64::MIN_POSITIVE);
let w_rel = ramp_down(
ratio.log10(),
CONDITIONING_GATE_RELATIVE.log10(),
CONDITIONING_GATE_RELATIVE_CLEAR.log10(),
);
w_abs.max(w_rel)
}
pub const CHEAP_CONDITIONING_PRECHECK_MIN_DIM: usize = 128;
const CHEAP_PRECHECK_SAFETY_MARGIN: f64 = 8.0;
const CHEAP_PRECHECK_LANCZOS_STEPS: usize = 12;
fn cheap_conditioning_bounds<HvFn>(mut hv: HvFn, p: usize) -> Result<Option<(f64, f64)>, String>
where
HvFn: FnMut(&Array1<f64>) -> Result<Array1<f64>, String>,
{
if p == 0 {
return Ok(None);
}
let steps = CHEAP_PRECHECK_LANCZOS_STEPS.min(p);
let mut q0 = vec![0.0_f64; p];
let golden = 0.618_033_988_749_894_8_f64; for (i, qi) in q0.iter_mut().enumerate() {
let frac = ((i as f64 + 1.0) * golden).fract();
*qi = frac - 0.5;
}
let q_norm = norm2_slice(&q0);
if !(q_norm.is_finite() && q_norm > 0.0) {
return Ok(None);
}
let mut hv_failed: Option<String> = None;
let eigen = match symmetric_lanczos_eigenpairs(
p,
&q0,
SymmetricLanczosOptions {
max_steps: steps,
residual_tol: f64::EPSILON,
local_reorthogonalize: false,
full_reorthogonalize: true,
},
|q, out| {
let qv = Array1::from_vec(q.to_vec());
let w = match hv(&qv) {
Ok(w) => w,
Err(e) => {
hv_failed = Some(e);
return Err("cheap_conditioning_bounds: HVP failed".to_string());
}
};
if w.len() != p || w.iter().any(|x| !x.is_finite()) {
return Err(
"cheap_conditioning_bounds: HVP produced non-finite/ill-sized output"
.to_string(),
);
}
out.copy_from_slice(w.as_slice().ok_or_else(|| {
"cheap_conditioning_bounds: HVP output not contiguous".to_string()
})?);
Ok(())
},
) {
Ok(eigen) => eigen,
Err(_) => {
if let Some(e) = hv_failed {
return Err(e);
}
return Ok(None);
}
};
let ritz = eigen.eigenvalues;
let ritz_vecs = eigen.eigenvectors;
let last_residual_norm = eigen.residual_norm;
let k = ritz.len();
if k == 0 {
return Ok(None);
}
let mut idx_min = 0usize;
let mut idx_max = 0usize;
for i in 1..k {
if ritz[i] < ritz[idx_min] {
idx_min = i;
}
if ritz[i] > ritz[idx_max] {
idx_max = i;
}
}
let theta_min = ritz[idx_min];
let theta_max = ritz[idx_max];
if !theta_min.is_finite() || !theta_max.is_finite() {
return Ok(None);
}
let last_row = k - 1;
let res_min = last_residual_norm * ritz_vecs[[last_row, idx_min]].abs();
let res_max = last_residual_norm * ritz_vecs[[last_row, idx_max]].abs();
let scale = theta_max.abs().max(1.0);
let converged_tol = 1e-3 * scale;
if res_min > converged_tol || res_max > converged_tol {
return Ok(None);
}
let lambda_min_lb = theta_min - res_min;
let lambda_max_ub = theta_max + res_max;
Ok(Some((lambda_min_lb, lambda_max_ub)))
}
pub fn jeffreys_term_skippable_via_matvec<HvFn>(hv: HvFn, p: usize) -> Result<bool, String>
where
HvFn: FnMut(&Array1<f64>) -> Result<Array1<f64>, String>,
{
if p < CHEAP_CONDITIONING_PRECHECK_MIN_DIM {
return Ok(false);
}
let (lambda_min_lb, lambda_max_ub) = match cheap_conditioning_bounds(hv, p)? {
Some(bounds) => bounds,
None => return Ok(false),
};
if !(lambda_min_lb.is_finite() && lambda_max_ub.is_finite()) {
return Ok(false);
}
if lambda_min_lb <= 0.0 || lambda_max_ub <= 0.0 {
return Ok(false);
}
let absolute_clears =
lambda_min_lb >= CHEAP_PRECHECK_SAFETY_MARGIN * CONDITIONING_GATE_ABSOLUTE_CLEAR;
let relative_clears = lambda_min_lb / lambda_max_ub
>= CHEAP_PRECHECK_SAFETY_MARGIN * CONDITIONING_GATE_RELATIVE_CLEAR;
Ok(absolute_clears && relative_clears)
}
#[derive(Debug, Clone)]
pub struct JeffreysSubspace {
pub columns: Array2<f64>,
}
impl JeffreysSubspace {
#[inline]
pub fn span_dim(&self) -> usize {
self.columns.ncols()
}
}
pub fn jeffreys_subspace_from_penalty(
aggregate_penalty: ArrayView2<'_, f64>,
) -> Result<JeffreysSubspace, String> {
let p = aggregate_penalty.nrows();
if aggregate_penalty.ncols() != p {
return Err(format!(
"jeffreys_subspace: aggregate penalty must be square, got {}x{}",
aggregate_penalty.nrows(),
aggregate_penalty.ncols()
));
}
if p == 0 {
return Ok(JeffreysSubspace {
columns: Array2::zeros((0, 0)),
});
}
Ok(JeffreysSubspace {
columns: Array2::eye(p),
})
}
pub fn joint_jeffreys_term<DirFn>(
h_joint: ArrayView2<'_, f64>,
z_j: ArrayView2<'_, f64>,
mut hessian_dir: DirFn,
) -> Result<(f64, Array1<f64>, Array2<f64>), String>
where
DirFn: FnMut(&Array1<f64>) -> Result<Option<Array2<f64>>, String>,
{
let p = h_joint.nrows();
if h_joint.ncols() != p {
return Err(format!(
"joint_jeffreys_term: H must be square, got {}x{}",
h_joint.nrows(),
h_joint.ncols()
));
}
if z_j.nrows() != p {
return Err(format!(
"joint_jeffreys_term: Z_J has {} rows, expected {} to match H",
z_j.nrows(),
p
));
}
let m = z_j.ncols();
if m == 0 {
return Ok((0.0, Array1::zeros(p), Array2::zeros((p, p))));
}
let hz = h_joint.dot(&z_j);
let h_id = z_j.t().dot(&hz);
let mut h_id_sym = Array2::<f64>::zeros((m, m));
for i in 0..m {
for j in 0..m {
h_id_sym[[i, j]] = 0.5 * (h_id[[i, j]] + h_id[[j, i]]);
}
}
let (evals, evecs) = h_id_sym.eigh(Side::Lower).map_err(|e| {
format!("joint_jeffreys_term: reduced-information eigendecomposition failed: {e}")
})?;
let lambda_max = evals.iter().cloned().fold(0.0_f64, f64::max);
let gate_weight = {
let lambda_min = evals.iter().cloned().fold(f64::INFINITY, f64::min);
conditioning_gate_weight(lambda_min, lambda_max)
};
if gate_weight == 0.0 {
return Ok((0.0, Array1::zeros(p), Array2::zeros((p, p))));
}
let floor = (REDUCED_INFO_RELATIVE_FLOOR * lambda_max).max(REDUCED_INFO_ABSOLUTE_FLOOR);
let mut phi = 0.0_f64;
let mut inv_diag = Array1::<f64>::zeros(m);
for (i, &lam) in evals.iter().enumerate() {
let lam_floored = lam.max(floor);
phi += 0.5 * lam_floored.ln();
inv_diag[i] = 1.0 / lam_floored;
}
let scaled = &evecs * &inv_diag.view().insert_axis(ndarray::Axis(0));
let h_id_inv = scaled.dot(&evecs.t());
let mut grad = Array1::<f64>::zeros(p);
let mut sensitivity = Array2::<f64>::zeros((p, m * m));
let mut axis = Array1::<f64>::zeros(p);
for k in 0..p {
axis.fill(0.0);
axis[k] = 1.0;
let hdot = match hessian_dir(&axis)? {
Some(hdot) => hdot,
None => {
return Ok((gate_weight * phi, Array1::zeros(p), Array2::zeros((p, p))));
}
};
if hdot.nrows() != p || hdot.ncols() != p {
return Err(format!(
"joint_jeffreys_term: Hdot shape {}x{} != {p}x{p}",
hdot.nrows(),
hdot.ncols()
));
}
let hdz = hdot.dot(&z_j);
let d_k = z_j.t().dot(&hdz);
let m_k = h_id_inv.dot(&d_k);
let mut trace = 0.0;
for i in 0..m {
trace += m_k[[i, i]];
}
grad[k] = 0.5 * trace;
let mut col = 0usize;
for i in 0..m {
for j in 0..m {
sensitivity[[k, col]] = m_k[[i, j]];
col += 1;
}
}
}
let mut hphi = Array2::<f64>::zeros((p, p));
for a in 0..p {
for b in a..p {
let mut acc = 0.0;
for col in 0..(m * m) {
acc += sensitivity[[a, col]] * sensitivity[[b, col]];
}
let value = 0.5 * acc;
hphi[[a, b]] = value;
hphi[[b, a]] = value;
}
}
Ok((gate_weight * phi, grad * gate_weight, hphi * gate_weight))
}
pub fn joint_jeffreys_hphi_directional_derivative<DirFn, Dir2Fn>(
h_joint: ArrayView2<'_, f64>,
z_j: ArrayView2<'_, f64>,
delta: &Array1<f64>,
mut hessian_dir: DirFn,
mut hessian_second_dir: Dir2Fn,
) -> Result<Array2<f64>, String>
where
DirFn: FnMut(&Array1<f64>) -> Result<Option<Array2<f64>>, String>,
Dir2Fn: FnMut(&Array1<f64>, &Array1<f64>) -> Result<Option<Array2<f64>>, String>,
{
let p = h_joint.nrows();
if h_joint.ncols() != p {
return Err(format!(
"joint_jeffreys_hphi_directional_derivative: H must be square, got {}x{}",
h_joint.nrows(),
h_joint.ncols()
));
}
if z_j.nrows() != p {
return Err(format!(
"joint_jeffreys_hphi_directional_derivative: Z_J has {} rows, expected {p}",
z_j.nrows()
));
}
if delta.len() != p {
return Err(format!(
"joint_jeffreys_hphi_directional_derivative: delta has {} entries, expected {p}",
delta.len()
));
}
let m = z_j.ncols();
if m == 0 || p == 0 {
return Ok(Array2::zeros((p, p)));
}
let hz0 = h_joint.dot(&z_j);
let h_id = z_j.t().dot(&hz0);
let mut h_id_sym = Array2::<f64>::zeros((m, m));
for i in 0..m {
for j in 0..m {
h_id_sym[[i, j]] = 0.5 * (h_id[[i, j]] + h_id[[j, i]]);
}
}
let (evals, evecs) = h_id_sym.eigh(Side::Lower).map_err(|e| {
format!("joint_jeffreys_hphi_directional_derivative: eigendecomposition failed: {e}")
})?;
let lambda_max = evals.iter().cloned().fold(0.0_f64, f64::max);
let gate_weight = {
let lambda_min = evals.iter().cloned().fold(f64::INFINITY, f64::min);
conditioning_gate_weight(lambda_min, lambda_max)
};
if gate_weight == 0.0 {
return Ok(Array2::zeros((p, p)));
}
let floor = (REDUCED_INFO_RELATIVE_FLOOR * lambda_max).max(REDUCED_INFO_ABSOLUTE_FLOOR);
let mut inv_diag = Array1::<f64>::zeros(m);
for (i, &lam) in evals.iter().enumerate() {
inv_diag[i] = 1.0 / lam.max(floor);
}
let scaled = &evecs * &inv_diag.view().insert_axis(ndarray::Axis(0));
let h_id_inv = scaled.dot(&evecs.t());
let hdot_delta = match hessian_dir(delta)? {
Some(hd) => hd,
None => return Ok(Array2::zeros((p, p))),
};
if hdot_delta.nrows() != p || hdot_delta.ncols() != p {
return Err(format!(
"joint_jeffreys_hphi_directional_derivative: Hdot[δ] shape {}x{} != {p}x{p}",
hdot_delta.nrows(),
hdot_delta.ncols()
));
}
let dbar = z_j.t().dot(&hdot_delta.dot(&z_j)); let k_dbar = h_id_inv.dot(&dbar);
let mut m_rows = Array2::<f64>::zeros((p, m * m)); let mut dm_rows = Array2::<f64>::zeros((p, m * m)); let mut axis = Array1::<f64>::zeros(p);
for a in 0..p {
axis.fill(0.0);
axis[a] = 1.0;
let hdot_a = match hessian_dir(&axis)? {
Some(hd) => hd,
None => return Ok(Array2::zeros((p, p))),
};
if hdot_a.nrows() != p || hdot_a.ncols() != p {
return Err(format!(
"joint_jeffreys_hphi_directional_derivative: Hdot[e_a] shape {}x{} != {p}x{p}",
hdot_a.nrows(),
hdot_a.ncols()
));
}
let d_a = z_j.t().dot(&hdot_a.dot(&z_j)); let m_a = h_id_inv.dot(&d_a);
let h2dot = match hessian_second_dir(delta, &axis)? {
Some(h2) => h2,
None => return Ok(Array2::zeros((p, p))),
};
if h2dot.nrows() != p || h2dot.ncols() != p {
return Err(format!(
"joint_jeffreys_hphi_directional_derivative: H²dot[δ,e_a] shape {}x{} != {p}x{p}",
h2dot.nrows(),
h2dot.ncols()
));
}
let d_a_delta = z_j.t().dot(&h2dot.dot(&z_j));
let dm_a = &h_id_inv.dot(&d_a_delta) - &k_dbar.dot(&m_a);
let mut col = 0usize;
for i in 0..m {
for j in 0..m {
m_rows[[a, col]] = m_a[[i, j]];
dm_rows[[a, col]] = dm_a[[i, j]];
col += 1;
}
}
}
let mut out = Array2::<f64>::zeros((p, p));
for a in 0..p {
for b in a..p {
let mut acc = 0.0;
for col in 0..(m * m) {
acc += dm_rows[[a, col]] * m_rows[[b, col]] + m_rows[[a, col]] * dm_rows[[b, col]];
}
let value = 0.5 * acc;
out[[a, b]] = value;
out[[b, a]] = value;
}
}
Ok(out * gate_weight)
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
fn conditioning_gate_skips(lambda_min: f64, lambda_max: f64) -> bool {
conditioning_gate_weight(lambda_min, lambda_max) == 0.0
}
#[test]
fn full_span_is_identity_regardless_of_penalty() {
for s in [
Array2::<f64>::zeros((3, 3)), {
let mut s = Array2::<f64>::zeros((3, 3));
s[[2, 2]] = 5.0; s
},
Array2::<f64>::eye(4) * 2.0, ] {
let p = s.nrows();
let z = jeffreys_subspace_from_penalty(s.view()).unwrap();
assert_eq!(z.span_dim(), p, "full span must equal the block dimension");
assert_eq!(z.columns.nrows(), p);
let gram = z.columns.t().dot(&z.columns);
for i in 0..p {
for j in 0..p {
let expected = if i == j { 1.0 } else { 0.0 };
assert!((gram[[i, j]] - expected).abs() < 1e-12);
}
}
}
}
#[test]
fn empty_block_yields_empty_span() {
let s = Array2::<f64>::zeros((0, 0));
let z = jeffreys_subspace_from_penalty(s.view()).unwrap();
assert_eq!(z.span_dim(), 0);
}
#[test]
fn joint_jeffreys_term_matches_finite_difference_gradient() {
let p = 2usize;
let ill = 1e-9_f64;
let z = Array2::<f64>::eye(p);
let h_at = |b: &Array1<f64>| -> Array2<f64> {
let mut h = Array2::<f64>::zeros((p, p));
h[[0, 0]] = b[0].exp();
h[[1, 1]] = ill * (1.0 + b[1] * b[1]);
h
};
let beta: Array1<f64> = array![0.3, -0.4];
let hdir = |d: &Array1<f64>| -> Result<Option<Array2<f64>>, String> {
let mut hd = Array2::<f64>::zeros((p, p));
hd[[0, 0]] = beta[0].exp() * d[0];
hd[[1, 1]] = ill * 2.0 * beta[1] * d[1];
Ok(Some(hd))
};
let h = h_at(&beta);
let (phi, grad, hphi) = joint_jeffreys_term(h.view(), z.view(), hdir).unwrap();
let expected_phi = 0.5 * (beta[0].exp() * ill * (1.0 + beta[1] * beta[1])).ln();
assert!(
(phi - expected_phi).abs() < 1e-6,
"phi {phi} vs {expected_phi}"
);
let eps = 1e-6;
for k in 0..p {
let mut bp = beta.clone();
let mut bm = beta.clone();
bp[k] += eps;
bm[k] -= eps;
let hp = h_at(&bp);
let hm = h_at(&bm);
let phi_p = 0.5 * (hp[[0, 0]] * hp[[1, 1]]).ln();
let phi_m = 0.5 * (hm[[0, 0]] * hm[[1, 1]]).ln();
let fd = (phi_p - phi_m) / (2.0 * eps);
assert!(
(grad[k] - fd).abs() < 1e-5,
"grad[{k}] {} vs fd {fd}",
grad[k]
);
}
for a in 0..p {
for b in 0..p {
assert!((hphi[[a, b]] - hphi[[b, a]]).abs() < 1e-12);
}
}
let (evals, _) = hphi.eigh(Side::Lower).unwrap();
for e in evals.iter() {
assert!(*e >= -1e-10, "H_Phi must be PSD, got eigenvalue {e}");
}
}
#[test]
fn conditioning_gate_skips_well_conditioned_information() {
let p = 2usize;
let z = Array2::<f64>::eye(p);
let mut h = Array2::<f64>::zeros((p, p));
h[[0, 0]] = 200.0;
h[[1, 1]] = 100.0; let hdir = |d: &Array1<f64>| -> Result<Option<Array2<f64>>, String> {
let mut hd = Array2::<f64>::zeros((p, p));
hd[[0, 0]] = 3.0 * d[0];
hd[[1, 1]] = 5.0 * d[1];
Ok(Some(hd))
};
let (phi, grad, hphi) = joint_jeffreys_term(h.view(), z.view(), hdir).unwrap();
assert_eq!(phi, 0.0, "well-conditioned ⇒ no Jeffreys value");
assert!(
grad.iter().all(|v| *v == 0.0),
"well-conditioned ⇒ zero grad"
);
assert!(
hphi.iter().all(|v| *v == 0.0),
"well-conditioned ⇒ zero curvature"
);
}
#[test]
fn conditioning_gate_fires_only_below_threshold() {
let p = 2usize;
let z = Array2::<f64>::eye(p);
let hdir = |d: &Array1<f64>| -> Result<Option<Array2<f64>>, String> {
let mut hd = Array2::<f64>::zeros((p, p));
hd[[0, 0]] = d[0];
hd[[1, 1]] = d[1];
Ok(Some(hd))
};
let mk = |lmin: f64| {
let mut h = Array2::<f64>::zeros((p, p));
h[[0, 0]] = 1.0;
h[[1, 1]] = lmin;
h
};
let mut above = mk(50.0);
above[[0, 0]] = 100.0;
let (phi_a, grad_a, _) = joint_jeffreys_term(above.view(), z.view(), hdir).unwrap();
assert_eq!(phi_a, 0.0);
assert!(grad_a.iter().all(|v| *v == 0.0));
let below_rel = mk(CONDITIONING_GATE_RELATIVE * 0.1);
let (phi_r, _g, hphi_r) = joint_jeffreys_term(below_rel.view(), z.view(), hdir).unwrap();
assert!(phi_r != 0.0, "relatively near-separating must fire");
assert!(hphi_r.iter().any(|v| v.abs() > 0.0));
let below_abs = mk(0.05);
let (phi_b, _grad_b, hphi_b) =
joint_jeffreys_term(below_abs.view(), z.view(), hdir).unwrap();
assert!(
phi_b != 0.0,
"absolutely near-separating (small-n) must fire even though the relative ratio clears the gate",
);
assert!(
hphi_b.iter().any(|v| v.abs() > 0.0),
"absolute-gate firing must produce nonzero bounding curvature",
);
}
#[test]
fn conditioning_gate_predicate_relative_and_absolute() {
assert!(conditioning_gate_skips(50.0, 100.0));
assert!(!conditioning_gate_skips(
CONDITIONING_GATE_RELATIVE * 0.1,
1.0
));
assert!(!conditioning_gate_skips(0.05, 1.0));
assert!(!conditioning_gate_skips(
CONDITIONING_GATE_ABSOLUTE,
CONDITIONING_GATE_ABSOLUTE
));
assert!(!conditioning_gate_skips(4.0, 100.0));
assert!(conditioning_gate_skips(
CONDITIONING_GATE_ABSOLUTE_CLEAR,
CONDITIONING_GATE_ABSOLUTE_CLEAR
));
assert!(!conditioning_gate_skips(0.0, 0.0));
assert!(!conditioning_gate_skips(f64::NAN, 100.0));
}
#[test]
fn conditioning_gate_weight_is_continuous_and_monotone() {
let lambda_max = 1.0e6; let w = |lmin: f64| conditioning_gate_weight(lmin, lambda_max);
assert_eq!(w(CONDITIONING_GATE_ABSOLUTE), 1.0);
assert_eq!(w(0.1), 1.0);
assert_eq!(w(CONDITIONING_GATE_ABSOLUTE_CLEAR), 0.0);
assert_eq!(w(100.0), 0.0);
let mut prev = 1.0;
let n = 200usize;
for k in 0..=n {
let lmin = CONDITIONING_GATE_ABSOLUTE
+ (CONDITIONING_GATE_ABSOLUTE_CLEAR - CONDITIONING_GATE_ABSOLUTE)
* (k as f64 / n as f64);
let cur = w(lmin);
assert!((0.0..=1.0).contains(&cur));
assert!(cur <= prev + 1e-12, "weight must be non-increasing");
assert!(
(prev - cur).abs() < 0.1,
"no large jumps across the smooth band (continuity)"
);
prev = cur;
}
}
#[test]
fn empty_span_yields_zero_term() {
let h = Array2::<f64>::eye(3);
let z = Array2::<f64>::zeros((3, 0));
let hdir = |_d: &Array1<f64>| -> Result<Option<Array2<f64>>, String> {
Ok(Some(Array2::<f64>::zeros((3, 3))))
};
let (phi, grad, hphi) = joint_jeffreys_term(h.view(), z.view(), hdir).unwrap();
assert_eq!(phi, 0.0);
assert!(grad.iter().all(|v| *v == 0.0));
assert!(hphi.iter().all(|v| *v == 0.0));
}
fn diag_hv(diag: Vec<f64>) -> impl FnMut(&Array1<f64>) -> Result<Array1<f64>, String> {
move |v: &Array1<f64>| {
let mut out = Array1::<f64>::zeros(v.len());
for (i, &d) in diag.iter().enumerate() {
out[i] = d * v[i];
}
Ok(out)
}
}
#[test]
fn cheap_precheck_skips_clearly_well_conditioned_large_p() {
let p = 200usize;
let mut diag = vec![220.0; p];
diag[0] = 200.0; diag[1] = 250.0; let skippable = jeffreys_term_skippable_via_matvec(diag_hv(diag), p).unwrap();
assert!(
skippable,
"clearly well-conditioned wide fit must be skippable"
);
}
#[test]
fn cheap_precheck_does_not_skip_near_separating() {
let p = 200usize;
let mut diag = vec![50.0; p];
diag[7] = 1e-3; let skippable = jeffreys_term_skippable_via_matvec(diag_hv(diag), p).unwrap();
assert!(
!skippable,
"a near-separating direction must NOT be skipped (term is needed)"
);
}
#[test]
fn cheap_precheck_does_not_skip_below_size_threshold() {
let p = CHEAP_CONDITIONING_PRECHECK_MIN_DIM - 1;
let diag = vec![100.0; p];
let skippable = jeffreys_term_skippable_via_matvec(diag_hv(diag), p).unwrap();
assert!(
!skippable,
"below the size threshold the pre-check never skips"
);
}
#[test]
fn cheap_precheck_does_not_skip_marginal_absolute() {
let p = 200usize;
let mut diag = vec![50.0; p];
diag[3] = 2.0;
let skippable = jeffreys_term_skippable_via_matvec(diag_hv(diag), p).unwrap();
assert!(
!skippable,
"λ_min within the 8× absolute margin must conservatively fall through"
);
}
#[test]
fn cheap_precheck_skip_implies_exact_gate_skips() {
let p = 150usize;
let z = Array2::<f64>::eye(p);
let hdir = |_d: &Array1<f64>| -> Result<Option<Array2<f64>>, String> {
Ok(Some(Array2::<f64>::zeros((p, p))))
};
for &lmin in &[10.0_f64, 25.0, 80.0, 200.0] {
let mut diag = vec![lmin * 4.0; p];
diag[0] = lmin;
let cheap_skip = jeffreys_term_skippable_via_matvec(diag_hv(diag.clone()), p).unwrap();
if cheap_skip {
let mut h = Array2::<f64>::zeros((p, p));
for (i, &d) in diag.iter().enumerate() {
h[[i, i]] = d;
}
let (phi, grad, hphi) = joint_jeffreys_term(h.view(), z.view(), hdir).unwrap();
assert_eq!(
phi, 0.0,
"cheap-skip ⇒ exact phi must be zero (byte-identical)"
);
assert!(grad.iter().all(|v| *v == 0.0));
assert!(hphi.iter().all(|v| *v == 0.0));
}
}
}
#[test]
fn cheap_precheck_bails_on_nonfinite_matvec() {
let p = 200usize;
let hv = |v: &Array1<f64>| -> Result<Array1<f64>, String> {
Ok(Array1::from_elem(v.len(), f64::NAN))
};
assert!(!jeffreys_term_skippable_via_matvec(hv, p).unwrap());
}
}