use super::*;
pub(crate) const PIRLS_INNER_TOLERANCE_FLOOR: f64 = 1e-6;
#[derive(Clone)]
pub(crate) struct RemlConfig {
pub(crate) likelihood: GlmLikelihoodSpec,
pub(crate) link_kind: InverseLink,
pub(crate) pirls_convergence_tolerance: f64,
pub(crate) max_iterations: usize,
pub(crate) reml_convergence_tolerance: f64,
pub(crate) firth_bias_reduction: bool,
pub(crate) geodesic_acceleration: bool,
}
impl RemlConfig {
pub(crate) fn external(
likelihood: GlmLikelihoodSpec,
reml_tol: f64,
firth_bias_reduction: bool,
) -> Self {
let pirls_tol = reml_tol.min(PIRLS_INNER_TOLERANCE_FLOOR);
let link_kind = likelihood.spec.link.clone();
Self {
likelihood,
link_kind,
pirls_convergence_tolerance: pirls_tol,
max_iterations: 0,
reml_convergence_tolerance: reml_tol,
firth_bias_reduction,
geodesic_acceleration: false,
}
.with_max_iterations(300)
}
pub(crate) fn with_max_iterations(mut self, max_iterations: usize) -> Self {
self.max_iterations = max_iterations;
self
}
pub(crate) fn link_function(&self) -> LinkFunction {
self.link_kind.link_function()
}
pub(crate) fn as_pirls_config(&self) -> pirls::PirlsConfig {
pirls::PirlsConfig {
likelihood: self.likelihood.clone(),
link_kind: self.link_kind.clone(),
max_iterations: self.max_iterations,
convergence_tolerance: self.pirls_convergence_tolerance,
firth_bias_reduction: self.firth_bias_reduction,
initial_lm_lambda: None,
geodesic_acceleration: self.geodesic_acceleration,
arrow_schur: None,
}
}
}
pub(crate) const MAX_FACTORIZATION_ATTEMPTS: usize = 4;
const LAML_RIDGE: f64 = 1e-8;
pub(crate) const DP_FLOOR: f64 = 1e-12;
const DP_FLOOR_SMOOTH_WIDTH: f64 = 1e-8;
pub(crate) const RHO_BOUND: f64 = 30.0;
pub(crate) const RHO_SOFT_PRIOR_WEIGHT: f64 = 1e-6;
pub(crate) const RHO_SOFT_PRIOR_SHARPNESS: f64 = 4.0;
pub(crate) const AUTO_CUBATURE_MAX_RHO_DIM: usize = 12;
pub(crate) const AUTO_CUBATURE_MAX_EIGENVECTORS: usize = 4;
pub(crate) const AUTO_CUBATURE_TARGET_VAR_FRAC: f64 = 0.95;
pub(crate) const AUTO_CUBATURE_MAX_BETA_DIM: usize = 1600;
pub(crate) const AUTO_CUBATURE_BOUNDARY_MARGIN: f64 = 2.0;
pub(crate) fn smooth_floor_dp(dp: f64, scale: f64) -> (f64, f64, f64) {
let scale = if scale.is_finite() && scale > 0.0 {
scale
} else {
1.0
};
let floor = DP_FLOOR * scale;
let tau = (DP_FLOOR_SMOOTH_WIDTH * scale).max(f64::MIN_POSITIVE);
let scaled = (dp - floor) / tau;
let softplus = if scaled > 20.0 {
scaled + (-scaled).exp()
} else if scaled < -20.0 {
scaled.exp()
} else {
(1.0 + scaled.exp()).ln()
};
let sigma = if scaled >= 0.0 {
let exp_neg = (-scaled).exp();
1.0 / (1.0 + exp_neg)
} else {
let exp_pos = scaled.exp();
exp_pos / (1.0 + exp_pos)
};
let dp_c = floor + tau * softplus;
let dp_cgrad2 = sigma * (1.0 - sigma) / tau;
(dp_c, sigma, dp_cgrad2)
}
pub(crate) struct SmoothingCorrectionComputation {
pub correction: Option<Array2<f64>>,
pub hessian_rho: Option<Array2<f64>>,
pub rho_covariance: Option<Array2<f64>>,
pub active_rank: Option<usize>,
}
pub(crate) struct InvertedRhoHessian {
pub inverse: Array2<f64>,
pub active_rank: usize,
pub dropped_negative: usize,
pub dropped_small_positive: usize,
pub dropped_numerical_zero: usize,
pub min_eigenvalue: f64,
pub repaired_hessian: bool,
pub eigenvalues: Array1<f64>,
pub eigenvectors: Array2<f64>,
pub classifications: Vec<EigenClassification>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum EigenClassification {
Active,
DroppedNegative,
DroppedSmallPositive,
DroppedNumericalZero,
}
pub(crate) fn invert_regularized_rho_hessian(
hessian_rho: &Array2<f64>,
) -> Option<InvertedRhoHessian> {
let n = hessian_rho.nrows();
if let Ok(chol) = hessian_rho.cholesky(faer::Side::Lower) {
let mut inverse = Array2::<f64>::eye(n);
for col in 0..n {
let colvec = inverse.column(col).to_owned();
let solved = chol.solvevec(&colvec);
inverse.column_mut(col).assign(&solved);
}
return Some(InvertedRhoHessian {
inverse,
active_rank: n,
dropped_negative: 0,
dropped_small_positive: 0,
dropped_numerical_zero: 0,
min_eigenvalue: f64::NAN,
repaired_hessian: false,
eigenvalues: Array1::<f64>::zeros(0),
eigenvectors: Array2::<f64>::zeros((0, 0)),
classifications: Vec::new(),
});
}
let (eigenvalues, eigenvectors) = hessian_rho.eigh(faer::Side::Lower).ok()?;
if eigenvalues.iter().any(|v| !v.is_finite()) || !eigenvectors.iter().all(|v| v.is_finite()) {
return None;
}
let spectral_scale = eigenvalues
.iter()
.copied()
.map(f64::abs)
.fold(0.0_f64, f64::max)
.max(1.0);
let min_eigenvalue = eigenvalues.iter().copied().fold(f64::INFINITY, f64::min);
let neg_tol = 64.0 * f64::EPSILON * (n.max(1) as f64) * spectral_scale;
let tau = (spectral_scale * 1e-10).max(LAML_RIDGE);
let mut inverse = Array2::<f64>::zeros((n, n));
let mut classifications = Vec::with_capacity(n);
let mut active_rank = 0usize;
let mut dropped_negative = 0usize;
let mut dropped_small_positive = 0usize;
let mut dropped_numerical_zero = 0usize;
for i in 0..n {
let sigma = eigenvalues[i];
let class = if sigma > tau {
EigenClassification::Active
} else if sigma.abs() <= neg_tol {
EigenClassification::DroppedNumericalZero
} else if sigma > 0.0 {
EigenClassification::DroppedSmallPositive
} else {
EigenClassification::DroppedNegative
};
classifications.push(class);
match class {
EigenClassification::Active => {
active_rank += 1;
let inv_lambda = 1.0 / sigma;
let v = eigenvectors.column(i);
for row in 0..n {
for col in 0..n {
inverse[[row, col]] += inv_lambda * v[row] * v[col];
}
}
}
EigenClassification::DroppedNegative => dropped_negative += 1,
EigenClassification::DroppedSmallPositive => dropped_small_positive += 1,
EigenClassification::DroppedNumericalZero => dropped_numerical_zero += 1,
}
}
Some(InvertedRhoHessian {
inverse,
active_rank,
dropped_negative,
dropped_small_positive,
dropped_numerical_zero,
min_eigenvalue,
repaired_hessian: active_rank < n,
eigenvalues,
eigenvectors,
classifications,
})
}
const INDEF_HESS_STRUCTURAL_REDUNDANCY_COS: f64 = 0.999;
const INDEF_HESS_PAIR_DUMP_GRID_MAX_K: usize = 16;
const INDEF_HESS_PAIR_DUMP_TOP_N: usize = 3;
fn dump_indefinite_rho_hessian_diagnostic(
hessian_rho: &Array2<f64>,
final_rho: &Array1<f64>,
canonical: &[crate::construction::CanonicalPenalty],
inverted: Option<&InvertedRhoHessian>,
) {
let k = hessian_rho.nrows();
if k == 0 {
return;
}
let (eigenvalues_owned, eigenvectors_owned);
let (eigenvalues_ref, eigenvectors_ref) = match inverted {
Some(inv) if !inv.eigenvalues.is_empty() && !inv.eigenvectors.is_empty() => {
(&inv.eigenvalues, &inv.eigenvectors)
}
_ => match hessian_rho.eigh(faer::Side::Lower) {
Ok((evals, evecs)) => {
eigenvalues_owned = evals;
eigenvectors_owned = evecs;
(&eigenvalues_owned, &eigenvectors_owned)
}
Err(err) => {
log::warn!("[INDEF-HESS] eigendecomposition failed: {err}");
return;
}
},
};
log::warn!("[INDEF-HESS] rho={:?}", final_rho.as_slice().unwrap_or(&[]),);
log::warn!(
"[INDEF-HESS] eigenvalues={:?}",
eigenvalues_ref.as_slice().unwrap_or(&[]),
);
if let Some(inv) = inverted {
log::warn!(
"[INDEF-HESS] active_rank={}/{} dropped_negative={} dropped_small_positive={} dropped_numerical_zero={}",
inv.active_rank,
k,
inv.dropped_negative,
inv.dropped_small_positive,
inv.dropped_numerical_zero,
);
if !inv.classifications.is_empty() {
let labels: Vec<&'static str> = inv
.classifications
.iter()
.map(|c| match c {
EigenClassification::Active => "A",
EigenClassification::DroppedNegative => "N",
EigenClassification::DroppedSmallPositive => "P",
EigenClassification::DroppedNumericalZero => "Z",
})
.collect();
log::warn!(
"[INDEF-HESS] classifications={:?} (A=active N=neg P=small_pos Z=numerical_zero)",
labels,
);
}
}
let mut neg_idx = 0usize;
let mut min_eig = f64::INFINITY;
for (i, &v) in eigenvalues_ref.iter().enumerate() {
if v < min_eig {
min_eig = v;
neg_idx = i;
}
}
let v_neg = eigenvectors_ref.column(neg_idx);
log::warn!(
"[INDEF-HESS] negative_eigenvalue={:.4e} eigenvector={:?}",
min_eig,
v_neg.as_slice().unwrap_or(&[]),
);
let n_pen = canonical.len();
let mut tr_aa = vec![0.0_f64; n_pen];
for i in 0..n_pen {
let local = &canonical[i].local;
let mut s = 0.0;
for r in 0..local.nrows() {
for c in 0..local.ncols() {
s += local[[r, c]] * local[[r, c]];
}
}
tr_aa[i] = s;
}
log::warn!(
"[INDEF-HESS] penalty_count={} ranges={:?} ranks={:?}",
n_pen,
(0..n_pen)
.map(|i| (canonical[i].col_range.start, canonical[i].col_range.end))
.collect::<Vec<_>>(),
(0..n_pen).map(|i| canonical[i].rank()).collect::<Vec<_>>(),
);
struct PairCos {
i: usize,
j: usize,
cos: f64,
antisym_proj: f64,
}
let mut pairs: Vec<PairCos> = Vec::new();
for i in 0..n_pen {
for j in (i + 1)..n_pen {
let ci = &canonical[i];
let cj = &canonical[j];
if ci.col_range != cj.col_range {
continue;
}
let local_i = &ci.local;
let local_j = &cj.local;
let mut dot = 0.0;
for r in 0..local_i.nrows() {
for c in 0..local_i.ncols() {
dot += local_i[[r, c]] * local_j[[r, c]];
}
}
let cos = if tr_aa[i] > 0.0 && tr_aa[j] > 0.0 {
dot / (tr_aa[i].sqrt() * tr_aa[j].sqrt())
} else {
f64::NAN
};
let antisym_proj = if v_neg.len() == n_pen {
(v_neg[i] - v_neg[j]) / std::f64::consts::SQRT_2
} else {
f64::NAN
};
pairs.push(PairCos {
i,
j,
cos,
antisym_proj,
});
}
}
if min_eig < 0.0 && v_neg.len() == n_pen {
for p in &pairs {
if !(p.cos > INDEF_HESS_STRUCTURAL_REDUNDANCY_COS) {
continue;
}
let mut indexed: Vec<(usize, f64)> = v_neg
.iter()
.enumerate()
.map(|(idx, &val)| (idx, val))
.collect();
indexed.sort_by(|a, b| {
b.1.abs()
.partial_cmp(&a.1.abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
if indexed.len() < 2 {
continue;
}
let top0 = indexed[0].0;
let top1 = indexed[1].0;
let (a, b) = if top0 == p.i && top1 == p.j {
(indexed[0].1, indexed[1].1)
} else if top0 == p.j && top1 == p.i {
(indexed[1].1, indexed[0].1)
} else {
continue;
};
if a * b >= 0.0 {
continue;
}
log::warn!(
"[INDEF-HESS] structural_redundancy_detected pair=({},{}) cos={:.6} antisym_proj={:.4e}",
p.i,
p.j,
p.cos,
p.antisym_proj,
);
break;
}
}
if n_pen <= INDEF_HESS_PAIR_DUMP_GRID_MAX_K {
for p in &pairs {
log::warn!(
"[INDEF-HESS] pair=({},{}) cos={:.6} tr_ii={:.4e} tr_jj={:.4e} v_neg[i]-v_neg[j]/sqrt2={:.4e}",
p.i,
p.j,
p.cos,
tr_aa[p.i],
tr_aa[p.j],
p.antisym_proj,
);
}
} else {
let mut top: Vec<&PairCos> = pairs.iter().filter(|p| p.cos.is_finite()).collect();
top.sort_by(|a, b| {
b.cos
.abs()
.partial_cmp(&a.cos.abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
for p in top.iter().take(INDEF_HESS_PAIR_DUMP_TOP_N) {
log::warn!(
"[INDEF-HESS] top_pair=({},{}) cos={:.6} tr_ii={:.4e} tr_jj={:.4e} v_neg[i]-v_neg[j]/sqrt2={:.4e}",
p.i,
p.j,
p.cos,
tr_aa[p.i],
tr_aa[p.j],
p.antisym_proj,
);
}
}
}
pub(crate) fn compute_smoothing_correction(
reml_state: &RemlState<'_>,
final_rho: &Array1<f64>,
final_fit: &pirls::PirlsResult,
) -> SmoothingCorrectionComputation {
use crate::faer_ndarray::{FaerCholesky, FaerEigh};
let n_rho = final_rho.len();
if n_rho == 0 {
return SmoothingCorrectionComputation {
correction: None,
hessian_rho: None,
rho_covariance: None,
active_rank: None,
};
}
let n_coeffs_trans = final_fit.beta_transformed.len();
let n_coeffs_orig = final_fit.reparam_result.qs.nrows();
let lambdas: Array1<f64> = final_rho.mapv(f64::exp);
let h_trans = reml_state
.objective_innerhessian(final_rho)
.unwrap_or_else(|_| final_fit.stabilizedhessian_transformed.to_dense());
if h_trans.nrows() != n_coeffs_trans || h_trans.ncols() != n_coeffs_trans {
log::warn!(
"smoothing-correction inner Hessian shape {}x{} does not match coefficient dimension {}; skipping.",
h_trans.nrows(),
h_trans.ncols(),
n_coeffs_trans
);
return SmoothingCorrectionComputation {
correction: None,
hessian_rho: None,
rho_covariance: None,
active_rank: None,
};
}
let h_chol = match h_trans.cholesky(faer::Side::Lower) {
Ok(c) => c,
Err(_) => {
log::warn!("Cholesky decomposition failed for smoothing correction; skipping.");
return SmoothingCorrectionComputation {
correction: None,
hessian_rho: None,
rho_covariance: None,
active_rank: None,
};
}
};
let beta_trans = final_fit.beta_transformed.as_ref();
let ct = &final_fit.reparam_result.canonical_transformed;
let mut dg_drho_trans = Array2::<f64>::zeros((n_coeffs_trans, n_rho));
let mut col_supports: Vec<std::ops::Range<usize>> = vec![0..0; n_rho];
for k in 0..n_rho {
if k >= ct.len() {
continue;
}
let cp = &ct[k];
if cp.rank() == 0 {
continue;
}
let r = &cp.col_range;
col_supports[k] = r.start..r.end;
let beta_block = beta_trans.slice(s![r.start..r.end]);
let centered = &beta_block - &cp.prior_mean;
let r_beta = cp.root.dot(¢ered);
for a in 0..cp.block_dim() {
dg_drho_trans[[r.start + a, k]] = lambdas[k]
* (0..cp.rank())
.map(|row| cp.root[[row, a]] * r_beta[row])
.sum::<f64>();
}
}
let jacobian_trans = match crate::solver::sensitivity::FitSensitivity::from_faer_cholesky(
&h_chol,
n_coeffs_trans,
)
.mode_response_coned(h_trans.view(), dg_drho_trans.view(), &col_supports)
{
Some(jacobian) => jacobian,
None => {
log::warn!("IFT beta-rho sensitivity solve failed for smoothing correction; skipping.");
return SmoothingCorrectionComputation {
correction: None,
hessian_rho: None,
rho_covariance: None,
active_rank: None,
};
}
};
let mut hessian_rho = match reml_state.compute_lamlhessian_consistent(final_rho) {
Ok(h) => h,
Err(err) => {
log::warn!(
"LAML Hessian unavailable ({}); skipping smoothing correction.",
err
);
return SmoothingCorrectionComputation {
correction: None,
hessian_rho: None,
rho_covariance: None,
active_rank: None,
};
}
};
crate::matrix::symmetrize_in_place(&mut hessian_rho);
add_relative_diag_ridge(&mut hessian_rho, LAML_RIDGE, LAML_RIDGE);
let inverted = match invert_regularized_rho_hessian(&hessian_rho) {
Some(inv) => inv,
None => {
log::warn!(
"Eigendecomposition of LAML rho Hessian failed for smoothing correction; skipping."
);
dump_indefinite_rho_hessian_diagnostic(
&hessian_rho,
final_rho,
&final_fit.reparam_result.canonical_transformed,
None,
);
return SmoothingCorrectionComputation {
correction: None,
hessian_rho: Some(hessian_rho),
rho_covariance: None,
active_rank: None,
};
}
};
let n_rho_total = hessian_rho.nrows();
if inverted.active_rank == 0 {
log::info!(
"LAML rho Hessian has no identified directions (active_rank=0/{}, dropped_negative={}, dropped_small_positive={}, dropped_numerical_zero={}, min_eig={:.3e}); smoothing correction is zero, skipping.",
n_rho_total,
inverted.dropped_negative,
inverted.dropped_small_positive,
inverted.dropped_numerical_zero,
inverted.min_eigenvalue,
);
dump_indefinite_rho_hessian_diagnostic(
&hessian_rho,
final_rho,
&final_fit.reparam_result.canonical_transformed,
Some(&inverted),
);
return SmoothingCorrectionComputation {
correction: None,
hessian_rho: Some(hessian_rho),
rho_covariance: Some(inverted.inverse),
active_rank: Some(0),
};
}
if inverted.active_rank < n_rho_total {
log::info!(
"LAML rho Hessian is rank-deficient on the identified subspace (active_rank={}/{}, dropped_negative={}, dropped_small_positive={}, dropped_numerical_zero={}, min_eig={:.3e}); proceeding with pseudo-inverse V_ρ.",
inverted.active_rank,
n_rho_total,
inverted.dropped_negative,
inverted.dropped_small_positive,
inverted.dropped_numerical_zero,
inverted.min_eigenvalue,
);
dump_indefinite_rho_hessian_diagnostic(
&hessian_rho,
final_rho,
&final_fit.reparam_result.canonical_transformed,
Some(&inverted),
);
}
let repaired_hessian = inverted.repaired_hessian;
let active_rank_used = inverted.active_rank;
let v_rho = inverted.inverse;
let rho_covariance = v_rho.clone();
if repaired_hessian {
log::debug!(
"Applied rank-deficient pseudo-inverse on identified rho-Hessian subspace before smoothing correction."
);
}
let jv_rho = jacobian_trans.dot(&v_rho); let v_corr_trans = jv_rho.dot(&jacobian_trans.t());
let qs = &final_fit.reparam_result.qs;
let qsv = qs.dot(&v_corr_trans);
let v_corr_orig = qsv.dot(&qs.t());
if !v_corr_orig.iter().all(|v| v.is_finite()) {
log::warn!("Non-finite values in smoothing correction matrix; skipping.");
return SmoothingCorrectionComputation {
correction: None,
hessian_rho: Some(hessian_rho),
rho_covariance: Some(rho_covariance),
active_rank: Some(active_rank_used),
};
}
match v_corr_orig.eigh(faer::Side::Lower) {
Ok((eigenvalues, _)) => {
let min_eig = eigenvalues.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let spectral_scale = eigenvalues
.iter()
.copied()
.map(f64::abs)
.fold(0.0_f64, f64::max)
.max(1.0);
let neg_tol = 64.0 * f64::EPSILON * (n_coeffs_orig.max(1) as f64) * spectral_scale;
if min_eig < -neg_tol {
log::warn!(
"Smoothing correction has material negative eigenvalue {:.3e} \
below tolerance {:.3e}; skipping correction.",
min_eig,
neg_tol
);
return SmoothingCorrectionComputation {
correction: None,
hessian_rho: Some(hessian_rho),
rho_covariance: Some(rho_covariance),
active_rank: Some(active_rank_used),
};
}
}
Err(_) => {
log::warn!("Eigendecomposition failed for smoothing correction validation.");
}
}
SmoothingCorrectionComputation {
correction: Some(v_corr_orig),
hessian_rho: Some(hessian_rho),
rho_covariance: Some(rho_covariance),
active_rank: Some(active_rank_used),
}
}