use super::*;
pub(crate) fn aggregate_labeled_hessian(
hessian: &Array2<f64>,
layout: &PenaltyLabelLayout,
) -> Result<Array2<f64>, String> {
if hessian.nrows() != layout.physical_count() || hessian.ncols() != layout.physical_count() {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"physical Hessian shape mismatch: got {}x{}, expected {}x{}",
hessian.nrows(),
hessian.ncols(),
layout.physical_count(),
layout.physical_count()
),
}
.into());
}
let mut out = Array2::<f64>::zeros((layout.initial_rho.len(), layout.initial_rho.len()));
for (i, oi) in layout.physical_to_outer.iter().enumerate() {
let Some(oi) = *oi else { continue };
for (j, oj) in layout.physical_to_outer.iter().enumerate() {
if let Some(oj) = *oj {
out[[oi, oj]] += hessian[[i, j]];
}
}
}
Ok(out)
}
pub(crate) fn rho_prior_cost_gradient_hessian(
prior: &crate::types::RhoPrior,
rho: &Array1<f64>,
) -> Result<(f64, Array1<f64>, Option<Array2<f64>>), String> {
use crate::rho_prior_eval::{InvalidPriorPolicy, RhoPriorError};
match crate::rho_prior_eval::evaluate(prior, rho, InvalidPriorPolicy::HardError) {
Ok(eval) => Ok((eval.cost, eval.gradient, eval.hessian)),
Err(RhoPriorError::DimensionMismatch { reason }) => {
Err(CustomFamilyError::DimensionMismatch { reason }.into())
}
Err(RhoPriorError::ConstraintViolation { reason }) => {
Err(CustomFamilyError::ConstraintViolation { reason }.into())
}
}
}
pub(crate) fn add_labeled_rho_prior_to_outer_eval(
mut result: OuterObjectiveEvalResult,
rho: &Array1<f64>,
rho_prior: &crate::types::RhoPrior,
eval_mode: EvalMode,
) -> Result<OuterObjectiveEvalResult, String> {
if matches!(rho_prior, crate::types::RhoPrior::Flat) {
return Ok(result);
}
let (cost, gradient, hessian) = rho_prior_cost_gradient_hessian(rho_prior, rho)?;
result.objective += cost;
if eval_mode != EvalMode::ValueOnly {
if result.gradient.len() != gradient.len() {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"rho prior gradient length mismatch: got {}, expected {}",
gradient.len(),
result.gradient.len()
),
}
.into());
}
result.gradient += &gradient;
}
if eval_mode == EvalMode::ValueGradientHessian
&& let Some(prior_hessian) = hessian
{
result.outer_hessian.add_rho_block_dense(&prior_hessian)?;
}
Ok(result)
}
pub(crate) fn physical_warm_start_for_labeled(
warm_start: Option<&ConstrainedWarmStart>,
physical_rho: &Array1<f64>,
layout: &PenaltyLabelLayout,
) -> Option<ConstrainedWarmStart> {
if !layout.has_tied_coordinates() {
return None;
}
warm_start.map(|seed| {
let mut physical_seed = seed.clone();
physical_seed.rho = physical_rho.clone();
physical_seed
})
}
pub(crate) fn pullback_labeled_outer_eval(
mut result: OuterObjectiveEvalResult,
rho: &Array1<f64>,
layout: &PenaltyLabelLayout,
rho_prior: &crate::types::RhoPrior,
eval_mode: EvalMode,
) -> Result<OuterObjectiveEvalResult, String> {
if eval_mode == EvalMode::ValueOnly {
result.gradient = Array1::<f64>::zeros(layout.initial_rho.len());
} else {
result.gradient = aggregate_labeled_gradient(&result.gradient, layout)?;
}
if eval_mode == EvalMode::ValueGradientHessian {
result.outer_hessian = match result.outer_hessian {
crate::solver::rho_optimizer::HessianResult::Analytic(hessian) => {
crate::solver::rho_optimizer::HessianResult::Analytic(aggregate_labeled_hessian(
&hessian, layout,
)?)
}
crate::solver::rho_optimizer::HessianResult::Operator(operator) => {
crate::solver::rho_optimizer::HessianResult::Operator(Arc::new(
LabeledOuterHessianOperator::new(operator, layout),
))
}
crate::solver::rho_optimizer::HessianResult::Unavailable => {
crate::solver::rho_optimizer::HessianResult::Unavailable
}
};
}
result.warm_start.rho = rho.clone();
add_labeled_rho_prior_to_outer_eval(result, rho, rho_prior, eval_mode)
}
pub(crate) fn outerobjectivegradienthessian_labeled<
F: CustomFamily + Clone + Send + Sync + 'static,
>(
family: &F,
specs: &[ParameterBlockSpec],
options: &BlockwiseFitOptions,
layout: &PenaltyLabelLayout,
rho: &Array1<f64>,
warm_start: Option<&ConstrainedWarmStart>,
rho_prior: &crate::types::RhoPrior,
eval_mode: EvalMode,
) -> Result<OuterObjectiveEvalResult, String> {
let physical_rho = expand_labeled_log_lambdas(rho, layout)?;
let physical_warm_start = physical_warm_start_for_labeled(warm_start, &physical_rho, layout);
let base = outerobjectivegradienthessian_internal(
family,
specs,
options,
&layout.penalty_counts,
&physical_rho,
physical_warm_start.as_ref().or(warm_start),
crate::types::RhoPrior::Flat,
eval_mode,
)?;
pullback_labeled_outer_eval(base, rho, layout, rho_prior, eval_mode)
}
pub(crate) fn custom_family_seed_screening_proxy_labeled<
F: CustomFamily + Clone + Send + Sync + 'static,
>(
family: &F,
specs: &[ParameterBlockSpec],
options: &BlockwiseFitOptions,
layout: &PenaltyLabelLayout,
rho: &Array1<f64>,
warm_start: Option<&ConstrainedWarmStart>,
rho_prior: &crate::types::RhoPrior,
) -> Result<(f64, ConstrainedWarmStart, bool), String> {
let physical_rho = expand_labeled_log_lambdas(rho, layout)?;
let per_block = split_log_lambdas(&physical_rho, &layout.penalty_counts)?;
let physical_warm_start = physical_warm_start_for_labeled(warm_start, &physical_rho, layout);
let screening_options = BlockwiseFitOptions {
seed_screening: true,
..options.clone()
};
let mut inner = inner_blockwise_fit(
family,
specs,
&per_block,
&screening_options,
physical_warm_start.as_ref().or(warm_start),
)?;
refresh_all_block_etas(family, specs, &mut inner.block_states)?;
let prior_terms = rho_prior_cost_gradient_hessian(rho_prior, rho)?;
let score = inner_penalized_objective(
&inner,
include_exact_newton_logdet_h(family, options),
include_exact_newton_logdet_s(family, options),
"custom-family labeled seed-screening proxy",
)? + prior_terms.0;
let warm = ConstrainedWarmStart {
rho: rho.clone(),
block_beta: inner
.block_states
.iter()
.map(|state| state.beta.clone())
.collect(),
active_sets: inner.active_sets.clone(),
cached_inner: Some(cached_inner_mode_from_result(&inner)),
};
Ok((score, warm, inner.converged))
}
pub(crate) fn split_log_lambdas(
flat: &Array1<f64>,
penalty_counts: &[usize],
) -> Result<Vec<Array1<f64>>, String> {
let expected: usize = penalty_counts.iter().sum();
if flat.len() != expected {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"log-lambda length mismatch: got {}, expected {expected}",
flat.len()
),
}
.into());
}
let mut out = Vec::with_capacity(penalty_counts.len());
let mut at = 0usize;
for &k in penalty_counts {
out.push(flat.slice(ndarray::s![at..at + k]).to_owned());
at += k;
}
Ok(out)
}
pub(crate) fn buildblock_states<F: CustomFamily + Clone + Send + Sync + 'static>(
family: &F,
specs: &[ParameterBlockSpec],
) -> Result<Vec<ParameterBlockState>, String> {
let mut states = Vec::with_capacity(specs.len());
for (b, spec) in specs.iter().enumerate() {
let p = spec.design.ncols();
let beta = spec
.initial_beta
.clone()
.unwrap_or_else(|| Array1::<f64>::zeros(p));
let eta = with_block_geometry(family, &states, spec, b, |x, off| {
let mut eta = x.matrixvectormultiply(&beta);
eta += off;
Ok(eta)
})?;
states.push(ParameterBlockState { beta, eta });
}
for b in 0..specs.len() {
let raw = states[b].beta.clone();
let projected = family.post_update_block_beta(&states, b, &specs[b], raw)?;
states[b].beta.assign(&projected);
}
Ok(states)
}
pub(crate) fn refresh_all_block_etas<F: CustomFamily + Clone + Send + Sync + 'static>(
family: &F,
specs: &[ParameterBlockSpec],
states: &mut [ParameterBlockState],
) -> Result<(), String> {
if family.block_geometry_is_dynamic() {
for b in 0..specs.len() {
refresh_single_block_eta(family, specs, states, b)?;
}
return Ok(());
}
use rayon::iter::{IntoParallelIterator, ParallelIterator};
let refreshed_etas: Vec<Array1<f64>> = (0..specs.len())
.into_par_iter()
.map(|b| {
specs[b]
.solver_design()
.matrixvectormultiply(&states[b].beta)
+ specs[b].solver_offset()
})
.collect();
for (state, eta) in states.iter_mut().zip(refreshed_etas) {
state.eta = eta;
}
Ok(())
}
pub(crate) fn refresh_single_block_eta<F: CustomFamily + Clone + Send + Sync + 'static>(
family: &F,
specs: &[ParameterBlockSpec],
states: &mut [ParameterBlockState],
block_idx: usize,
) -> Result<(), String> {
let spec = &specs[block_idx];
let beta = states[block_idx].beta.clone();
states[block_idx].eta = with_block_geometry(family, states, spec, block_idx, |x, off| {
Ok(x.matrixvectormultiply(&beta) + off)
})?;
Ok(())
}
#[inline]
pub(crate) fn capped_inner_max_cycles(options: &BlockwiseFitOptions, base_cycles: usize) -> usize {
let mut cap = base_cycles;
if let Some(screening) = options.screening_max_inner_iterations.as_ref() {
let screening_cap = screening.load(Ordering::Relaxed);
if screening_cap > 0 {
cap = cap.min(screening_cap);
}
}
if let Some(outer) = options.outer_inner_max_iterations.as_ref() {
let outer_cap = outer.load(Ordering::Relaxed);
if outer_cap > 0 {
cap = cap.min(outer_cap);
}
}
cap.max(1)
}
pub(crate) fn weighted_normal_equations(
x: &DesignMatrix,
w: &Array1<f64>,
y_star: Option<&Array1<f64>>,
) -> Result<(Array2<f64>, Option<Array1<f64>>), String> {
let n = x.nrows();
if w.len() != n {
return Err(CustomFamilyError::DimensionMismatch {
reason: "weighted normal-equation dimension mismatch".to_string(),
}
.into());
}
if let Some(y) = y_star
&& y.len() != n
{
return Err(CustomFamilyError::DimensionMismatch {
reason: "weighted RHS dimension mismatch".to_string(),
}
.into());
}
let xtwx = x.xt_diag_x_signed_op(SignedWeightsView::from_array(w))?;
let xtwy = if let Some(y) = y_star {
Some(x.compute_xtwy(w, y)?)
} else {
None
};
Ok((xtwx, xtwy))
}
pub(crate) fn exact_newton_stabilizing_shift_psd_penalized(
combined: &Array2<f64>,
gershgorin_src: &Array2<f64>,
ridge_floor: f64,
) -> Option<f64> {
stabilizing_shift_core(combined, gershgorin_src, ridge_floor)
}
fn stabilizing_shift_core(
cholesky_test: &Array2<f64>,
gershgorin_src: &Array2<f64>,
ridge_floor: f64,
) -> Option<f64> {
let floor = effective_solverridge(ridge_floor);
if cholesky_test.cholesky(Side::Lower).is_ok() {
return None;
}
let p = gershgorin_src.nrows();
let mut gershgorin_min = f64::INFINITY;
for i in 0..p {
let diag = gershgorin_src[[i, i]];
let mut radius = 0.0_f64;
for j in 0..p {
if j != i {
radius += gershgorin_src[[i, j]].abs();
}
}
gershgorin_min = gershgorin_min.min(diag - radius);
}
if !gershgorin_min.is_finite() {
let diag_max = (0..cholesky_test.nrows())
.map(|d| cholesky_test[[d, d]].abs())
.fold(0.0_f64, f64::max);
return Some(floor.max(diag_max * 1e-6).max(1e-6));
}
if gershgorin_min >= floor {
return Some(floor);
}
Some(floor - gershgorin_min)
}
pub(crate) fn stabilize_exact_newton_penalized_lhs_in_place<F: CustomFamily + ?Sized>(
family: &F,
lhs_dense: &mut Array2<f64>,
data_hessian_gershgorin_src: &Array2<f64>,
ridge_floor: f64,
) {
if use_exact_newton_strict_spd(family) {
return;
}
if let Some(shift) = exact_newton_stabilizing_shift_psd_penalized(
lhs_dense,
data_hessian_gershgorin_src,
ridge_floor,
) {
for d in 0..lhs_dense.nrows() {
lhs_dense[[d, d]] += shift;
}
}
}
pub(crate) fn shift_linear_constraints_to_delta(
constraints: &LinearInequalityConstraints,
beta: &Array1<f64>,
) -> Result<LinearInequalityConstraints, String> {
if constraints.a.ncols() != beta.len() || constraints.a.nrows() != constraints.b.len() {
return Err(CustomFamilyError::ConstraintViolation {
reason: "linear constraints: shape mismatch".to_string(),
}
.into());
}
Ok(LinearInequalityConstraints {
a: constraints.a.clone(),
b: &constraints.b - &constraints.a.dot(beta),
})
}
pub(crate) fn collect_block_linear_constraints<F: CustomFamily + ?Sized>(
family: &F,
states: &[ParameterBlockState],
specs: &[ParameterBlockSpec],
) -> Result<Vec<Option<LinearInequalityConstraints>>, String> {
let mut constraints = Vec::with_capacity(specs.len());
for (block_idx, spec) in specs.iter().enumerate() {
constraints.push(family.block_linear_constraints(states, block_idx, spec)?);
}
Ok(constraints)
}
pub(crate) fn reject_constrained_post_update_repair(
block_idx: usize,
spec: &ParameterBlockSpec,
raw_beta: &Array1<f64>,
updated_beta: &Array1<f64>,
constraints: Option<&LinearInequalityConstraints>,
) -> Result<(), String> {
let Some(constraints) = constraints else {
return Ok(());
};
if raw_beta.len() != updated_beta.len() {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"post-update beta length changed for constrained block '{}' (idx {block_idx}): raw={}, updated={}",
spec.name,
raw_beta.len(),
updated_beta.len(),
),
}
.into());
}
if raw_beta.len() != constraints.a.ncols() {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"post-update constrained block '{}' (idx {block_idx}) width mismatch: beta={}, constraints={}",
spec.name,
raw_beta.len(),
constraints.a.ncols(),
),
}
.into());
}
let max_change = raw_beta
.iter()
.zip(updated_beta.iter())
.map(|(left, right)| (left - right).abs())
.fold(0.0_f64, f64::max);
let raw_scale = raw_beta.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
let updated_scale = updated_beta.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
let tol = 1e-10 * (1.0 + raw_scale.max(updated_scale));
if max_change > tol {
return Err(CustomFamilyError::ConstraintViolation {
reason: format!(
"post-update hook materially changed constrained block '{}' (idx {block_idx}): \
max |β_post - β_qp|={max_change:.3e} > tol={tol:.3e}; \
constraints must be represented analytically in block_linear_constraints, not repaired after the Newton/QP solve",
spec.name,
),
}
.into());
}
Ok(())
}
pub(crate) fn assemble_joint_linear_constraints(
block_constraints: &[Option<LinearInequalityConstraints>],
ranges: &[(usize, usize)],
total_p: usize,
) -> Result<Option<LinearInequalityConstraints>, String> {
if block_constraints.len() != ranges.len() {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"joint linear constraint assembly mismatch: {} blocks but {} ranges",
block_constraints.len(),
ranges.len()
),
}
.into());
}
let total_rows = block_constraints
.iter()
.map(|constraints| constraints.as_ref().map_or(0, |c| c.a.nrows()))
.sum::<usize>();
if total_rows == 0 {
return Ok(None);
}
let mut a = Array2::<f64>::zeros((total_rows, total_p));
let mut b = Array1::<f64>::zeros(total_rows);
let mut row_offset = 0usize;
for (block_idx, constraints_opt) in block_constraints.iter().enumerate() {
let Some(constraints) = constraints_opt else {
continue;
};
let (start, end) = ranges[block_idx];
let block_p = end - start;
if constraints.a.ncols() != block_p || constraints.a.nrows() != constraints.b.len() {
return Err(CustomFamilyError::DimensionMismatch { reason: format!(
"joint linear constraint assembly mismatch for block {block_idx}: A is {}x{}, b is {}, block width is {}",
constraints.a.nrows(),
constraints.a.ncols(),
constraints.b.len(),
block_p
) }.into());
}
let rows = constraints.a.nrows();
a.slice_mut(s![row_offset..(row_offset + rows), start..end])
.assign(&constraints.a);
b.slice_mut(s![row_offset..(row_offset + rows)])
.assign(&constraints.b);
row_offset += rows;
}
Ok(Some(LinearInequalityConstraints { a, b }))
}
pub(crate) fn flatten_joint_active_set(
block_active_sets: &[Option<Vec<usize>>],
block_constraints: &[Option<LinearInequalityConstraints>],
) -> Option<Vec<usize>> {
if block_active_sets.len() != block_constraints.len() {
return None;
}
let mut offset = 0usize;
let mut joint_active = Vec::new();
for (active_opt, constraints_opt) in block_active_sets.iter().zip(block_constraints.iter()) {
let rows = constraints_opt
.as_ref()
.map_or(0, |constraints| constraints.a.nrows());
if let Some(active) = active_opt {
joint_active.extend(
active
.iter()
.copied()
.filter(|&idx| idx < rows)
.map(|idx| offset + idx),
);
}
offset += rows;
}
if joint_active.is_empty() {
None
} else {
Some(joint_active)
}
}
pub(crate) fn scatter_joint_active_set(
joint_active: &[usize],
block_constraints: &[Option<LinearInequalityConstraints>],
) -> Vec<Option<Vec<usize>>> {
let mut per_block = Vec::with_capacity(block_constraints.len());
let mut offset = 0usize;
for constraints_opt in block_constraints {
let rows = constraints_opt
.as_ref()
.map_or(0, |constraints| constraints.a.nrows());
if rows == 0 {
per_block.push(None);
continue;
}
let mut local = joint_active
.iter()
.copied()
.filter(|&idx| idx >= offset && idx < offset + rows)
.map(|idx| idx - offset)
.collect::<Vec<_>>();
offset += rows;
if local.is_empty() {
per_block.push(None);
continue;
}
local.sort_unstable();
local.dedup();
per_block.push(Some(local));
}
per_block
}
pub(crate) fn augment_active_sets_with_tight_constraint_rows(
block_active_sets: &mut Vec<Option<Vec<usize>>>,
block_constraints: &[Option<LinearInequalityConstraints>],
states: &[ParameterBlockState],
slack_tol: f64,
) -> usize {
if block_active_sets.len() != block_constraints.len() {
block_active_sets.resize(block_constraints.len(), None);
}
let mut added = 0usize;
let tol = slack_tol.max(0.0);
for (b, constraints_opt) in block_constraints.iter().enumerate() {
let Some(constraints) = constraints_opt else {
continue;
};
let Some(state) = states.get(b) else {
continue;
};
if constraints.a.ncols() != state.beta.len() {
continue;
}
let active = block_active_sets[b].get_or_insert_with(Vec::new);
for row in 0..constraints.a.nrows() {
let slack = constraints.a.row(row).dot(&state.beta) - constraints.b[row];
if slack.is_finite() && slack <= tol && !active.contains(&row) {
active.push(row);
added += 1;
}
}
if active.is_empty() {
block_active_sets[b] = None;
} else {
active.sort_unstable();
active.dedup();
}
}
added
}
pub(crate) fn assemble_active_constraint_block(
block_constraints: &[Option<LinearInequalityConstraints>],
block_active_sets: &[Option<Vec<usize>>],
ranges: &[(usize, usize)],
total_p: usize,
) -> Option<ActiveLinearConstraintBlock> {
if block_constraints.len() != ranges.len() || block_active_sets.len() != ranges.len() {
return None;
}
let mut active_per_block: Vec<(usize, &[usize], &LinearInequalityConstraints)> = Vec::new();
let mut total_active = 0usize;
for (b, (range, (constraints_opt, active_opt))) in ranges
.iter()
.zip(block_constraints.iter().zip(block_active_sets.iter()))
.enumerate()
{
let Some(constraints) = constraints_opt else {
continue;
};
let Some(active) = active_opt else {
continue;
};
if active.is_empty() {
continue;
}
if constraints.a.ncols() != range.1 - range.0 {
return None;
}
if !active.iter().all(|&r| r < constraints.a.nrows()) {
return None;
}
total_active += active.len();
active_per_block.push((b, active.as_slice(), constraints));
}
if total_active == 0 {
return None;
}
let mut a = ndarray::Array2::<f64>::zeros((total_active, total_p));
let mut out_row = 0usize;
for (b_idx, active, constraints) in active_per_block {
let (start, end) = ranges[b_idx];
let block_p = end - start;
for &local_row in active {
for col in 0..block_p {
a[[out_row, start + col]] = constraints.a[[local_row, col]];
}
out_row += 1;
}
}
Some(ActiveLinearConstraintBlock { a })
}
pub(crate) struct SimpleLowerBounds {
pub(crate) lower_bounds: Array1<f64>,
pub(crate) row_to_coeff: Vec<usize>,
pub(crate) coeff_to_row: Vec<Option<usize>>,
}
pub(crate) fn extract_simple_lower_bounds(
constraints: &LinearInequalityConstraints,
p: usize,
) -> Result<Option<SimpleLowerBounds>, String> {
if constraints.a.ncols() != p || constraints.a.nrows() != constraints.b.len() {
return Err(CustomFamilyError::ConstraintViolation {
reason: "linear constraints: shape mismatch".to_string(),
}
.into());
}
let mut lower_bounds = Array1::from_elem(p, f64::NEG_INFINITY);
let mut coeff_to_row = vec![None; p];
let mut row_to_coeff = Vec::with_capacity(constraints.a.nrows());
for row in 0..constraints.a.nrows() {
let mut coeff_idx = None;
let mut coeff_value = 0.0;
for col in 0..p {
let value = constraints.a[[row, col]];
if value.abs() <= 1e-12 {
continue;
}
if coeff_idx.is_some() {
return Ok(None);
}
coeff_idx = Some(col);
coeff_value = value;
}
let Some(col) = coeff_idx else {
return Ok(None);
};
if coeff_value <= 0.0 {
return Ok(None);
}
let bound = constraints.b[row] / coeff_value;
if bound > lower_bounds[col] {
lower_bounds[col] = bound;
coeff_to_row[col] = Some(row);
}
row_to_coeff.push(col);
}
Ok(Some(SimpleLowerBounds {
lower_bounds,
row_to_coeff,
coeff_to_row,
}))
}
pub(crate) fn lower_bound_active_rows_to_coeffs(
bounds: &SimpleLowerBounds,
active_rows: Option<&[usize]>,
) -> Vec<usize> {
let Some(active_rows) = active_rows else {
return Vec::new();
};
let mut active_coeffs = active_rows
.iter()
.copied()
.filter_map(|row| bounds.row_to_coeff.get(row).copied())
.collect::<Vec<_>>();
active_coeffs.sort_unstable();
active_coeffs.dedup();
active_coeffs
}
pub(crate) fn lower_bound_active_coeffs_to_rows(
bounds: &SimpleLowerBounds,
active_coeffs: &[usize],
) -> Vec<usize> {
let mut active_rows = active_coeffs
.iter()
.copied()
.filter_map(|coeff| bounds.coeff_to_row.get(coeff).and_then(|row| *row))
.collect::<Vec<_>>();
active_rows.sort_unstable();
active_rows.dedup();
active_rows
}
pub(crate) fn lower_bound_active_coeffs_from_solution(
bounds: &SimpleLowerBounds,
beta: &Array1<f64>,
) -> Vec<usize> {
let mut active_coeffs = Vec::new();
for coeff in 0..beta.len() {
let lower = bounds.lower_bounds[coeff];
if !lower.is_finite() {
continue;
}
let scale = beta[coeff].abs().max(lower.abs()).max(1.0);
let tol = 1e-6 * scale + 1e-10;
if beta[coeff] <= lower + tol {
active_coeffs.push(coeff);
}
}
active_coeffs
}
pub(crate) fn project_to_lower_bounds(beta: &mut Array1<f64>, lower_bounds: &Array1<f64>) {
for i in 0..beta.len() {
let lower = lower_bounds[i];
if lower.is_finite() && beta[i] < lower {
beta[i] = lower;
}
}
}
pub(crate) fn solve_quadratic_with_simple_lower_bounds(
lhs: &Array2<f64>,
rhs: &Array1<f64>,
beta_start: &Array1<f64>,
bounds: &SimpleLowerBounds,
active_rows: Option<&[usize]>,
) -> Result<(Array1<f64>, Vec<usize>), String> {
let gradient = lhs.dot(beta_start) - rhs;
let mut delta = Array1::zeros(beta_start.len());
let mut active_coeffs = lower_bound_active_rows_to_coeffs(bounds, active_rows);
solve_newton_directionwith_lower_bounds(
lhs,
&gradient,
beta_start,
&bounds.lower_bounds,
&mut delta,
Some(&mut active_coeffs),
)
.map_err(|e| format!("lower-bound Newton solve failed: {e}"))?;
let mut beta_new = beta_start + δ
project_to_lower_bounds(&mut beta_new, &bounds.lower_bounds);
active_coeffs = lower_bound_active_coeffs_from_solution(bounds, &beta_new);
let active = lower_bound_active_coeffs_to_rows(bounds, &active_coeffs);
Ok((beta_new, active))
}
pub(crate) fn normalize_active_set(mut active_set: Vec<usize>) -> Option<Vec<usize>> {
active_set.sort_unstable();
active_set.dedup();
if active_set.is_empty() {
None
} else {
Some(active_set)
}
}
pub(crate) fn normalize_active_sets(
active_sets: Vec<Option<Vec<usize>>>,
) -> Vec<Option<Vec<usize>>> {
active_sets
.into_iter()
.map(|active_set| active_set.and_then(normalize_active_set))
.collect()
}
pub(crate) struct BlockUpdateContext<'a> {
pub(crate) family: &'a dyn CustomFamily,
pub(crate) states: &'a [ParameterBlockState],
pub(crate) spec: &'a ParameterBlockSpec,
pub(crate) block_idx: usize,
pub(crate) s_lambda: &'a Array2<f64>,
pub(crate) options: &'a BlockwiseFitOptions,
pub(crate) linear_constraints: Option<&'a LinearInequalityConstraints>,
pub(crate) cached_active_set: Option<&'a [usize]>,
}
pub(crate) struct BlockUpdateResult {
pub(crate) beta_new_raw: Array1<f64>,
pub(crate) active_set: Option<Vec<usize>>,
}
#[inline]
pub(crate) fn floor_positiveworking_weights(
working_weights: &Array1<f64>,
minweight: f64,
) -> Array1<f64> {
let mut out = Array1::<f64>::zeros(working_weights.len());
ndarray::Zip::from(&mut out)
.and(working_weights)
.par_for_each(|o, &wi| *o = if wi <= 0.0 { 0.0 } else { wi.max(minweight) });
out
}
pub(crate) trait ParameterBlockUpdater {
fn compute_update_step(
&self,
ctx: &BlockUpdateContext<'_>,
) -> Result<BlockUpdateResult, String>;
}
pub(crate) struct DiagonalBlockUpdater<'a> {
pub(crate) working_response: &'a Array1<f64>,
pub(crate) working_weights: &'a Array1<f64>,
}
impl ParameterBlockUpdater for DiagonalBlockUpdater<'_> {
fn compute_update_step(
&self,
ctx: &BlockUpdateContext<'_>,
) -> Result<BlockUpdateResult, String> {
if self.working_response.len() != ctx.spec.design.nrows()
|| self.working_weights.len() != ctx.spec.design.nrows()
{
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"family diagonal working-set size mismatch on block {} ({})",
ctx.block_idx, ctx.spec.name
),
}
.into());
}
let w_clamped = floor_positiveworking_weights(self.working_weights, ctx.options.minweight);
if let Some(constraints) = ctx.linear_constraints {
check_linear_feasibility(&ctx.states[ctx.block_idx].beta, constraints, 1e-8).map_err(
|e| {
format!(
"block {} ({}) constrained diagonal solve: {e}",
ctx.block_idx, ctx.spec.name
)
},
)?;
with_block_geometry(ctx.family, ctx.states, ctx.spec, ctx.block_idx, |x, off| {
let mut y_star = self.working_response.clone();
y_star -= off;
let (mut lhs, rhs_opt) = weighted_normal_equations(x, &w_clamped, Some(&y_star))?;
let rhs = rhs_opt.ok_or_else(|| {
"missing weighted RHS in constrained diagonal solve".to_string()
})?;
lhs += ctx.s_lambda;
let lower_bounds = extract_simple_lower_bounds(constraints, lhs.ncols())?;
let (beta_constrained, active_set) = if let Some(bounds) = lower_bounds.as_ref() {
solve_quadratic_with_simple_lower_bounds(
&lhs,
&rhs,
&ctx.states[ctx.block_idx].beta,
bounds,
ctx.cached_active_set,
)
} else {
solve_quadratic_with_linear_constraints(
&lhs,
&rhs,
&ctx.states[ctx.block_idx].beta,
constraints,
ctx.cached_active_set,
)
.map_err(|e| e.to_string())
}
.map_err(|e| {
format!(
"block {} ({}) constrained diagonal solve failed: {e}",
ctx.block_idx, ctx.spec.name
)
})?;
Ok(BlockUpdateResult {
beta_new_raw: beta_constrained,
active_set: normalize_active_set(active_set),
})
})
} else {
with_block_geometry(ctx.family, ctx.states, ctx.spec, ctx.block_idx, |x, off| {
let n = self.working_response.len();
let wy = Array1::from_shape_fn(n, |i| {
(self.working_response[i] - off[i]) * w_clamped[i].max(0.0)
});
let xtwy = x.transpose_vector_multiply(&wy);
let beta = x
.solve_systemwith_policy(
&w_clamped,
&xtwy,
Some(ctx.s_lambda),
ctx.options.ridge_floor,
ctx.options.ridge_policy,
)
.map_err(|_| "block solve failed after ridge retries".to_string())?;
Ok(BlockUpdateResult {
beta_new_raw: beta,
active_set: None,
})
})
}
}
}
pub(crate) struct ExactNewtonBlockUpdater<'a> {
pub(crate) gradient: &'a Array1<f64>,
pub(crate) hessian: &'a SymmetricMatrix,
}
impl ParameterBlockUpdater for ExactNewtonBlockUpdater<'_> {
fn compute_update_step(
&self,
ctx: &BlockUpdateContext<'_>,
) -> Result<BlockUpdateResult, String> {
let p = ctx.spec.design.ncols();
if self.gradient.len() != p {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"block {} exact-newton gradient length mismatch: got {}, expected {p}",
ctx.block_idx,
self.gradient.len()
),
}
.into());
}
if self.hessian.nrows() != p || self.hessian.ncols() != p {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"block {} exact-newton Hessian shape mismatch: got {}x{}, expected {}x{}",
ctx.block_idx,
self.hessian.nrows(),
self.hessian.ncols(),
p,
p
),
}
.into());
}
let lhs = self.hessian.add_dense(ctx.s_lambda)?;
let rhs_step = self.gradient - &ctx.s_lambda.dot(&ctx.states[ctx.block_idx].beta);
let mut lhs_dense = lhs.to_dense();
let data_hessian_dense = self.hessian.to_dense();
stabilize_exact_newton_penalized_lhs_in_place(
ctx.family,
&mut lhs_dense,
&data_hessian_dense,
ctx.options.ridge_floor,
);
if let Some(constraints) = ctx.linear_constraints {
check_linear_feasibility(&ctx.states[ctx.block_idx].beta, constraints, 1e-8).map_err(
|e| {
format!(
"block {} ({}) constrained exact-newton solve: {e}",
ctx.block_idx, ctx.spec.name
)
},
)?;
let lower_bounds = extract_simple_lower_bounds(constraints, p).map_err(|e| {
format!(
"block {} ({}) constrained exact-newton solve: {e}",
ctx.block_idx, ctx.spec.name
)
})?;
let (beta_new_raw, active_set) = if let Some(bounds) = lower_bounds.as_ref() {
let rhs_beta = &lhs_dense.dot(&ctx.states[ctx.block_idx].beta) + &rhs_step;
solve_quadratic_with_simple_lower_bounds(
&lhs_dense,
&rhs_beta,
&ctx.states[ctx.block_idx].beta,
bounds,
ctx.cached_active_set,
)
} else {
let delta_constraints =
shift_linear_constraints_to_delta(constraints, &ctx.states[ctx.block_idx].beta)
.map_err(|e| {
format!(
"block {} ({}) constrained exact-newton solve: {e}",
ctx.block_idx, ctx.spec.name
)
})?;
let delta_start = Array1::zeros(p);
let (delta, active_set) = solve_quadratic_with_linear_constraints(
&lhs_dense,
&rhs_step,
&delta_start,
&delta_constraints,
ctx.cached_active_set,
)
.map_err(|e| e.to_string())?;
Ok((&ctx.states[ctx.block_idx].beta + &delta, active_set))
}
.map_err(|e| {
format!(
"block {} ({}) constrained exact-newton solve failed: {e}",
ctx.block_idx, ctx.spec.name
)
})?;
Ok(BlockUpdateResult {
beta_new_raw,
active_set: normalize_active_set(active_set),
})
} else {
let delta = if use_exact_newton_strict_spd(ctx.family) {
let (step, lm_stats) =
strict_solve_spd_with_lm_continuation(&lhs_dense, &rhs_step)?;
if lm_stats.escalations > 0 {
log::debug!(
"[strict-spd-lm] block={} ({}): δ-ridge continuation succeeded \
after {} escalation(s) at δ={:.3e}",
ctx.block_idx,
ctx.spec.name,
lm_stats.escalations,
lm_stats.delta_used,
);
}
step
} else {
let step = match strict_solve_spd_with_lm_continuation(&lhs_dense, &rhs_step) {
Ok((step, lm_stats)) => {
if lm_stats.escalations > 0 {
log::debug!(
"[joint-Newton/lm] block={} ({}): non-strict δ-ridge continuation \
succeeded after {} escalation(s) at δ={:.3e}",
ctx.block_idx,
ctx.spec.name,
lm_stats.escalations,
lm_stats.delta_used,
);
}
step
}
Err(_) => (0..lhs_dense.nrows())
.map(|i| {
let d = lhs_dense[[i, i]].abs().max(1e-8);
rhs_step[i] / d
})
.collect(),
};
step
};
let beta = &ctx.states[ctx.block_idx].beta + δ
Ok(BlockUpdateResult {
beta_new_raw: beta,
active_set: None,
})
}
}
}
impl BlockWorkingSet {
pub(crate) fn updater(&self) -> Box<dyn ParameterBlockUpdater + '_> {
match self {
BlockWorkingSet::Diagonal {
working_response,
working_weights,
} => Box::new(DiagonalBlockUpdater {
working_response,
working_weights,
}),
BlockWorkingSet::ExactNewton { gradient, hessian } => {
Box::new(ExactNewtonBlockUpdater { gradient, hessian })
}
}
}
}
pub(crate) fn check_linear_feasibility(
beta: &Array1<f64>,
constraints: &LinearInequalityConstraints,
tol: f64,
) -> Result<(), String> {
if constraints.a.ncols() != beta.len() || constraints.a.nrows() != constraints.b.len() {
return Err(CustomFamilyError::ConstraintViolation {
reason: "linear constraints: shape mismatch".to_string(),
}
.into());
}
let slack = constraints.a.dot(beta) - &constraints.b;
let mut worst = 0.0_f64;
let mut worst_idx = 0usize;
for (i, &s) in slack.iter().enumerate() {
let v = (-s).max(0.0);
if v > worst {
worst = v;
worst_idx = i;
}
}
if worst > tol {
let a_worst = constraints.a.row(worst_idx);
let norm_worst = a_worst.dot(&a_worst).sqrt();
let scaled_worst = if norm_worst > 0.0 {
worst / norm_worst
} else {
worst
};
let beta_inf = beta.iter().fold(0.0_f64, |m, &v| m.max(v.abs()));
let interior_outcome =
match crate::solver::active_set::project_point_strictly_into_feasible_cone(
beta,
constraints,
) {
Some(p) => {
let s = constraints.a.dot(&p) - &constraints.b;
let w = s.iter().fold(0.0_f64, |m, &v| m.max((-v).max(0.0)));
format!("strict-interior→worst={w:.3e}")
}
None => "strict-interior→None".to_string(),
};
let boundary_outcome = {
let identity = Array2::<f64>::eye(beta.len());
match crate::solver::active_set::solve_quadratic_with_linear_constraints(
&identity,
beta,
beta,
constraints,
None,
) {
Ok((p, _)) => {
let s = constraints.a.dot(&p) - &constraints.b;
let w = s.iter().fold(0.0_f64, |m, &v| m.max((-v).max(0.0)));
format!("boundary→worst={w:.3e}")
}
Err(e) => format!("boundary→Err({e})"),
}
};
let worst_row_nnz = (0..constraints.a.ncols())
.filter(|&c| constraints.a[[worst_idx, c]].abs() > 1e-12)
.count();
let simple_bounds_path = match extract_simple_lower_bounds(constraints, beta.len()) {
Ok(Some(_)) => "extract_simple_lower_bounds→Some(SIMPLE-BOUNDS PATH)",
Ok(None) => "extract_simple_lower_bounds→None(general QP)",
Err(_) => "extract_simple_lower_bounds→Err",
};
return Err(CustomFamilyError::ConstraintViolation {
reason: format!(
"infeasible iterate: max(Aβ-b violation)={worst:.3e} at constraint row {worst_idx} \
[#1108 diag: rows={}, worst_row_nnz={worst_row_nnz}, ‖a_row‖={norm_worst:.3e}, \
scaled_viol={scaled_worst:.3e}, |β|∞={beta_inf:.3e}, {interior_outcome}, \
{boundary_outcome}, {simple_bounds_path}]",
constraints.a.nrows()
),
}
.into());
}
Ok(())
}
#[inline]
pub(crate) fn effective_solverridge(ridge_floor: f64) -> f64 {
ridge_floor.max(1e-15)
}
pub(crate) fn block_quadratic_penalty(
beta: &Array1<f64>,
s_lambda: &Array2<f64>,
ridge: f64,
ridge_policy: RidgePolicy,
) -> f64 {
let mut value = 0.5 * beta.dot(&s_lambda.dot(beta));
if ridge_policy.include_quadratic_penalty {
value += 0.5 * ridge * beta.dot(beta);
}
value
}
pub(crate) fn block_penalized_hessian_vector(
spec: &ParameterBlockSpec,
work: &BlockWorkingSet,
s_lambda: &Array2<f64>,
direction: &Array1<f64>,
ridge: f64,
ridge_policy: RidgePolicy,
) -> Array1<f64> {
let mut hpen = match work {
BlockWorkingSet::ExactNewton { hessian, .. } => hessian.dot(direction),
BlockWorkingSet::Diagonal {
working_weights, ..
} => {
let solver_design = spec.solver_design();
let x_direction = solver_design.matrixvectormultiply(direction);
let wx_direction = &x_direction * working_weights;
solver_design.transpose_vector_multiply(&wx_direction)
}
};
hpen += &s_lambda.dot(direction);
if ridge_policy.include_quadratic_penalty && ridge > 0.0 {
hpen.scaled_add(ridge, direction);
}
hpen
}
pub(crate) fn symmetric_matrix_diagonal(matrix: &SymmetricMatrix) -> Array1<f64> {
match matrix {
SymmetricMatrix::Dense(mat) => mat.diag().to_owned(),
SymmetricMatrix::Sparse(mat) => {
let mut out = Array1::<f64>::zeros(mat.ncols());
let (symbolic, values) = mat.parts();
let col_ptr = symbolic.col_ptr();
let row_idx = symbolic.row_idx();
for col in 0..mat.ncols() {
for idx in col_ptr[col]..col_ptr[col + 1] {
if row_idx[idx] == col {
out[col] += values[idx];
}
}
}
out
}
}
}
pub(crate) fn block_penalized_metric_diagonal(
spec: &ParameterBlockSpec,
work: &BlockWorkingSet,
s_lambda: &Array2<f64>,
ridge: f64,
ridge_policy: RidgePolicy,
) -> Result<Array1<f64>, String> {
let mut diagonal = match work {
BlockWorkingSet::ExactNewton { hessian, .. } => symmetric_matrix_diagonal(hessian),
BlockWorkingSet::Diagonal {
working_weights, ..
} => spec.design.diag_gram(working_weights)?,
};
if diagonal.len() != s_lambda.nrows() || s_lambda.nrows() != s_lambda.ncols() {
return Err(format!(
"block penalized metric diagonal shape mismatch: diag={}, S={}x{}",
diagonal.len(),
s_lambda.nrows(),
s_lambda.ncols()
));
}
for j in 0..diagonal.len() {
diagonal[j] += s_lambda[[j, j]];
if ridge_policy.include_quadratic_penalty && ridge > 0.0 {
diagonal[j] += ridge;
}
diagonal[j] = positive_joint_diagonal_entry(diagonal[j]);
}
Ok(diagonal)
}
pub(crate) fn block_penalized_metric_norm(
spec: &ParameterBlockSpec,
work: &BlockWorkingSet,
s_lambda: &Array2<f64>,
direction: &Array1<f64>,
ridge: f64,
ridge_policy: RidgePolicy,
) -> Result<f64, String> {
let diagonal = block_penalized_metric_diagonal(spec, work, s_lambda, ridge, ridge_policy)?;
if diagonal.len() != direction.len() {
return Err(format!(
"block penalized metric direction length mismatch: direction={}, diag={}",
direction.len(),
diagonal.len()
));
}
Ok(joint_trust_region_metric_step_norm(direction, &diagonal))
}
pub(crate) fn truncate_block_step_to_metric_radius(
spec: &ParameterBlockSpec,
work: &BlockWorkingSet,
s_lambda: &Array2<f64>,
delta: Array1<f64>,
radius: f64,
ridge: f64,
ridge_policy: RidgePolicy,
) -> Result<(Array1<f64>, f64), String> {
let norm = block_penalized_metric_norm(spec, work, s_lambda, &delta, ridge, ridge_policy)?;
if norm.is_finite() && norm > radius && radius > 0.0 {
Ok((&delta * (radius / norm), radius))
} else {
Ok((delta, norm))
}
}
pub(crate) const TOTAL_QUADRATIC_PENALTY_PAR_MIN_BLOCKS: usize = 4;
pub(crate) const TOTAL_QUADRATIC_PENALTY_PAR_MIN_DENSE_WORK: usize = 16_384;
pub(crate) fn total_quadratic_penalty_parallel_worthwhile(
states: &[ParameterBlockState],
s_lambdas: &[Array2<f64>],
) -> bool {
let n_blocks = states.len().min(s_lambdas.len());
if n_blocks < TOTAL_QUADRATIC_PENALTY_PAR_MIN_BLOCKS || rayon::current_num_threads() <= 1 {
return false;
}
states
.iter()
.zip(s_lambdas.iter())
.map(|(state, s_lambda)| {
let p = state.beta.len().min(s_lambda.ncols());
p.saturating_mul(s_lambda.nrows())
})
.try_fold(0usize, |acc, work| {
let next = acc.saturating_add(work);
(next < TOTAL_QUADRATIC_PENALTY_PAR_MIN_DENSE_WORK).then_some(next)
})
.is_none()
}
pub(crate) fn total_quadratic_penalty(
states: &[ParameterBlockState],
s_lambdas: &[Array2<f64>],
ridge: f64,
ridge_policy: RidgePolicy,
joint_full_width: Option<&crate::families::joint_penalty::JointPenaltyBundle>,
specs: Option<&[ParameterBlockSpec]>,
) -> f64 {
let per_block: f64 = if total_quadratic_penalty_parallel_worthwhile(states, s_lambdas) {
use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
states
.par_iter()
.zip(s_lambdas.par_iter())
.map(|(state, s_lambda)| {
block_quadratic_penalty(&state.beta, s_lambda, ridge, ridge_policy)
})
.reduce(|| 0.0, |left, right| left + right)
} else {
states
.iter()
.zip(s_lambdas.iter())
.map(|(state, s_lambda)| {
block_quadratic_penalty(&state.beta, s_lambda, ridge, ridge_policy)
})
.sum()
};
let joint = match (joint_full_width, specs) {
(Some(bundle), Some(specs)) if !bundle.is_empty() => {
let beta_flat = flatten_state_betas(states, specs);
bundle.quadratic(beta_flat.view())
}
_ => 0.0,
};
per_block + joint
}
pub(crate) fn smooth_regularized_logdet_hessian_finite_check(
matrix: &Array2<f64>,
block: Option<usize>,
) -> Result<(), String> {
let Some((row, col, value)) = matrix
.indexed_iter()
.find_map(|((row, col), &value)| (!value.is_finite()).then_some((row, col, value)))
else {
return Ok(());
};
let block_context = match block {
Some(b) => format!(" for block {b}"),
None => String::new(),
};
Err(CustomFamilyError::NumericalFailure { reason: format!(
"smooth-regularized logdet Hessian contains non-finite entry at ({row}, {col}): {value}{block_context}"
) }.into())
}
pub(crate) fn validate_block_hessians_finite(eval: &FamilyEvaluation) -> Result<(), String> {
for (b, ws) in eval.blockworking_sets.iter().enumerate() {
let BlockWorkingSet::ExactNewton { hessian, .. } = ws else {
continue;
};
match hessian {
SymmetricMatrix::Dense(matrix) => {
smooth_regularized_logdet_hessian_finite_check(matrix, Some(b))?;
}
SymmetricMatrix::Sparse(matrix) => {
let (symbolic, values) = matrix.parts();
let col_ptr = symbolic.col_ptr();
let row_idx = symbolic.row_idx();
for col in 0..matrix.ncols() {
let start = col_ptr[col];
let end = col_ptr[col + 1];
for idx in start..end {
let row = row_idx[idx];
let value = values[idx];
if !value.is_finite() {
return Err(CustomFamilyError::NumericalFailure { reason: format!(
"smooth-regularized logdet Hessian contains non-finite entry at ({row}, {col}): {value} for block {b}"
) }.into());
}
}
}
}
}
}
Ok(())
}
pub(crate) fn stable_logdet_with_ridge_policy(
matrix: &Array2<f64>,
ridge_floor: f64,
ridge_policy: RidgePolicy,
) -> Result<f64, String> {
let mut a = matrix.clone();
symmetrize_dense_in_place(&mut a);
let p = a.nrows();
let ridge = if ridge_policy.include_penalty_logdet {
effective_solverridge(ridge_floor)
} else {
0.0
};
for i in 0..p {
a[[i, i]] += ridge;
}
match resolved_ridge_determinant_mode(ridge_policy, p) {
RidgeDeterminantMode::Full => {
let chol = a.cholesky(Side::Lower).map_err(|_| {
"cholesky failed while computing full ridge-aware logdet".to_string()
})?;
Ok(2.0 * chol.diag().mapv(f64::ln).sum())
}
RidgeDeterminantMode::Auto => Err(
"internal: resolved_ridge_determinant_mode must resolve Auto to a concrete mode"
.to_string(),
),
RidgeDeterminantMode::PositivePart => {
smooth_regularized_logdet_hessian_finite_check(&a, None)?;
match crate::faer_ndarray::FaerEigh::eigh(&a, Side::Lower) {
Ok((evals, _)) => {
let eval_vec: Vec<f64> = evals
.as_slice()
.map(|sl| sl.to_vec())
.unwrap_or_else(|| evals.iter().copied().collect());
let eps = spectral_epsilon(&eval_vec)
.max(ridge.max(CUSTOM_FAMILY_CONDITION_RELATIVE_FLOOR));
let n_negative = eval_vec.iter().filter(|&&ev| ev < -eps).count();
if n_negative > 0 {
log::debug!(
"[SmoothRegularizedLogdet] Hessian has {n_negative} \
eigenvalue(s) below -eps={eps:.2e}; r_ε damps them \
smoothly instead of dropping them."
);
}
let logdet: f64 = eval_vec
.iter()
.map(|&sigma| spectral_regularize(sigma, eps).ln())
.sum();
Ok(logdet)
}
Err(eigh_err) => Err(CustomFamilyError::BasisDecompositionFailed {
reason: format!(
"smooth-regularized logdet eigendecomposition failed: {eigh_err}"
),
}
.into()),
}
}
}
}
pub(crate) fn try_cholesky_with_escalating_ridge<R>(
matrix: &Array2<f64>,
initial_boost: f64,
max_attempts: usize,
growth: f64,
mut on_success: impl FnMut(&crate::faer_ndarray::FaerCholeskyFactor, usize, f64) -> Option<R>,
) -> Option<(R, f64, usize)> {
let p = matrix.nrows();
let mut boost = initial_boost;
for attempt in 0..max_attempts {
let mut candidate = matrix.clone();
if boost != 0.0 {
for i in 0..p {
candidate[[i, i]] += boost;
}
}
if let Ok(chol) = candidate.cholesky(Side::Lower)
&& let Some(r) = on_success(&chol, attempt, boost)
{
return Some((r, boost, attempt));
}
boost *= growth;
}
None
}
pub(crate) fn penalty_logdet_cholesky_fallback(
s_ridged: &Array2<f64>,
existing_ridge: f64,
block: usize,
p: usize,
eigh_err: &str,
) -> Result<f64, String> {
let diag_scale = s_ridged
.diag()
.iter()
.copied()
.map(f64::abs)
.fold(0.0_f64, f64::max)
.max(1.0);
const MAX_ATTEMPTS: usize = 6;
let initial_boost = diag_scale * 1e-8;
let outcome = try_cholesky_with_escalating_ridge(
s_ridged,
initial_boost,
MAX_ATTEMPTS,
10.0,
|chol, attempt, boost| {
let logdet = 2.0 * chol.diag().mapv(f64::ln).sum();
if logdet.is_finite() {
log::warn!(
"[PenaltyLogdetFallback] eigendecomposition failed for block {block} \
({eigh_err}); using Cholesky with boosted ridge={:.2e} \
(attempt {}/{MAX_ATTEMPTS}, existing_ridge={:.2e}, p={p})",
boost + existing_ridge,
attempt + 1,
existing_ridge,
);
Some(logdet)
} else {
None
}
},
);
if let Some((logdet, _, _)) = outcome {
return Ok(logdet);
}
let final_boost = initial_boost * 10.0_f64.powi(MAX_ATTEMPTS as i32);
Err(CustomFamilyError::BasisDecompositionFailed {
reason: format!(
"penalty logdet eigendecomposition failed for block {block} ({eigh_err}) and \
Cholesky fallback also failed after {MAX_ATTEMPTS} attempts \
(final ridge={:.2e}, p={p})",
final_boost + existing_ridge,
),
}
.into())
}
pub(crate) fn resolved_ridge_determinant_mode(
ridge_policy: RidgePolicy,
dim: usize,
) -> RidgeDeterminantMode {
assert!(
dim.checked_add(1).is_some(),
"ridge determinant dimension overflow"
);
match ridge_policy.determinant_mode {
RidgeDeterminantMode::Auto => RidgeDeterminantMode::Full,
mode => mode,
}
}
pub(crate) fn inverse_spdwith_retry(
matrix: &Array2<f64>,
baseridge: f64,
max_retry: usize,
) -> Result<Array2<f64>, String> {
let mut sym = matrix.clone();
symmetrize_dense_in_place(&mut sym);
let invert_via_chol = |chol: &crate::faer_ndarray::FaerCholeskyFactor, _: usize, _: f64| {
let mut ident = Array2::<f64>::eye(sym.nrows());
chol.solve_mat_in_place(&mut ident);
symmetrize_dense_in_place(&mut ident);
Some(ident)
};
if let Some((inv, _, _)) =
try_cholesky_with_escalating_ridge(&sym, 0.0, 1, 1.0, invert_via_chol)
{
return Ok(inv);
}
if max_retry > 0
&& let Some((inv, _, _)) =
try_cholesky_with_escalating_ridge(&sym, baseridge, max_retry, 10.0, invert_via_chol)
{
return Ok(inv);
}
Err(CustomFamilyError::BasisDecompositionFailed {
reason: "failed to invert SPD system after Cholesky ridge retries".to_string(),
}
.into())
}
pub(crate) fn symmetrize_dense_in_place(matrix: &mut Array2<f64>) {
crate::linalg::matrix::symmetrize_in_place(matrix);
}
pub(crate) fn validate_flat_direction_length(
direction: &Array1<f64>,
expected: usize,
context: &str,
) -> Result<(), String> {
if direction.len() != expected {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"{context}: direction length mismatch: got {}, expected {expected}",
direction.len()
),
}
.into());
}
Ok::<(), _>(())
}
pub(crate) fn strict_solve_spd(
matrix: &Array2<f64>,
rhs: &Array1<f64>,
) -> Result<Array1<f64>, String> {
let mut sym = matrix.clone();
symmetrize_dense_in_place(&mut sym);
let chol = sym
.cholesky(Side::Lower)
.map_err(|_| "strict pseudo-laplace SPD solve failed".to_string())?;
Ok(chol.solvevec(rhs))
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct StrictSpdLmStats {
pub(crate) delta_used: f64,
pub(crate) escalations: usize,
}
pub(crate) const STRICT_SPD_LM_MAX_ESCALATIONS: usize = 16;
pub(crate) const STRICT_SPD_LM_RIDGE_GROWTH: f64 = 10.0;
pub(crate) const CUSTOM_FAMILY_WEIGHT_FLOOR: f64 = crate::types::MIN_WEIGHT;
pub(crate) const CUSTOM_FAMILY_RIDGE_FLOOR: f64 = 1e-12;
pub(crate) const CUSTOM_FAMILY_EVAL_FLOOR: f64 = 1e-12;
pub(crate) const CUSTOM_FAMILY_CONDITION_RELATIVE_FLOOR: f64 = 1e-14;
pub(crate) fn strict_spd_lm_engine<R>(
matrix: &Array2<f64>,
op_label: &'static str,
empty: R,
bare_path: impl FnOnce(&Array2<f64>) -> Result<R, String>,
process_chol: impl FnOnce(&crate::faer_ndarray::FaerCholeskyFactor) -> R,
process_eigen: impl FnOnce(&Array1<f64>, &Array2<f64>, f64) -> R,
) -> Result<(R, StrictSpdLmStats), String> {
if let Ok(r) = bare_path(matrix) {
return Ok((r, StrictSpdLmStats::default()));
}
let p = matrix.nrows();
if p == 0 {
return Ok((empty, StrictSpdLmStats::default()));
}
let mut sym = matrix.clone();
symmetrize_dense_in_place(&mut sym);
let trace_scale = (0..p).map(|i| sym[[i, i]].abs()).sum::<f64>() / (p as f64);
let delta0 = (f64::EPSILON * trace_scale.max(1.0)).max(CUSTOM_FAMILY_RIDGE_FLOOR);
let mut delta = delta0;
for escalation in 1..=STRICT_SPD_LM_MAX_ESCALATIONS {
let mut ridged = sym.clone();
for i in 0..p {
ridged[[i, i]] += delta;
}
if let Ok(chol) = ridged.cholesky(Side::Lower) {
return Ok((
process_chol(&chol),
StrictSpdLmStats {
delta_used: delta,
escalations: escalation,
},
));
}
delta *= STRICT_SPD_LM_RIDGE_GROWTH;
}
let max_esc = STRICT_SPD_LM_MAX_ESCALATIONS;
let (evals, evecs) = FaerEigh::eigh(&sym, Side::Lower).map_err(|e| {
format!(
"{op_label} failed even with LM δ-ridge continuation \
(escalated {max_esc} times to δ={delta:.3e}, trace_scale={trace_scale:.3e}); \
eigen-floor fallback also failed: {e}"
)
})?;
let max_abs_eval = evals.iter().fold(0.0_f64, |a, &b| a.max(b.abs()));
let eps_floor = (CUSTOM_FAMILY_EVAL_FLOOR * max_abs_eval).max(1e-300);
Ok((
process_eigen(&evals, &evecs, eps_floor),
StrictSpdLmStats {
delta_used: delta,
escalations: STRICT_SPD_LM_MAX_ESCALATIONS + 1,
},
))
}
pub(crate) fn strict_solve_spd_with_lm_continuation(
matrix: &Array2<f64>,
rhs: &Array1<f64>,
) -> Result<(Array1<f64>, StrictSpdLmStats), String> {
let p = matrix.nrows();
strict_spd_lm_engine(
matrix,
"strict pseudo-laplace SPD solve",
Array1::<f64>::zeros(0),
|m| strict_solve_spd(m, rhs),
|chol| chol.solvevec(rhs),
|evals, evecs, eps_floor| {
let mut q_t_rhs = Array1::<f64>::zeros(p);
for k in 0..p {
let mut acc = 0.0;
for i in 0..p {
acc += evecs[[i, k]] * rhs[i];
}
q_t_rhs[k] = acc / evals[k].max(eps_floor);
}
let mut x = Array1::<f64>::zeros(p);
for i in 0..p {
let mut acc = 0.0;
for k in 0..p {
acc += evecs[[i, k]] * q_t_rhs[k];
}
x[i] = acc;
}
x
},
)
}
pub(crate) fn strict_exact_pseudo_logdet(
matrix: &Array2<f64>,
accumulation_depth: usize,
) -> Result<f64, String> {
let mut sym = matrix.clone();
symmetrize_dense_in_place(&mut sym);
let (evals, _) = FaerEigh::eigh(&sym, Side::Lower)
.map_err(|e| format!("strict pseudo-laplace eigendecomposition failed: {e}"))?;
let p = sym.nrows();
let max_abs_eval = evals.iter().fold(0.0_f64, |acc, &ev| acc.max(ev.abs()));
let eps = f64::EPSILON;
let eps_np = eps * (accumulation_depth as f64) * (p as f64);
let neg_tol = (10.0 * eps_np * max_abs_eval).max(100.0 * eps);
let pos_tol = positive_eigenvalue_threshold(evals.as_slice().unwrap());
if evals.iter().any(|&ev| ev < -neg_tol) {
let min_eval = evals.iter().copied().fold(f64::INFINITY, f64::min);
let below = evals.iter().filter(|&&ev| ev < -neg_tol).count();
return Err(CustomFamilyError::NumericalFailure {
reason: format!(
"strict pseudo-laplace logdet: {below} eigenvalue(s) below -neg_tol \
(min(λ)={min_eval:.6e}, max|λ|={max_abs_eval:.6e}, neg_tol={neg_tol:.6e}, εnp={eps_np:.6e}); \
indefinite joint coefficient Hessian rejected (no δ-ridge masking, gam#748)"
),
}
.into());
}
Ok(evals
.iter()
.copied()
.filter(|&ev| ev > pos_tol)
.map(f64::ln)
.sum())
}
pub(crate) fn pinv_positive_part(
matrix: &Array2<f64>,
ridge_floor: f64,
) -> Result<Array2<f64>, String> {
let mut sym = matrix.clone();
symmetrize_dense_in_place(&mut sym);
let (eigenvalues, eigenvectors) = sym
.eigh(Side::Lower)
.map_err(|e| format!("positive-part covariance eigendecomposition failed: {e}"))?;
let max_abs_eigenvalue = eigenvalues.iter().fold(0.0_f64, |a, &b| a.max(b.abs()));
let tol = (max_abs_eigenvalue * CUSTOM_FAMILY_EVAL_FLOOR)
.max(ridge_floor.max(CUSTOM_FAMILY_CONDITION_RELATIVE_FLOOR));
let p = matrix.nrows();
let mut pinv = Array2::<f64>::zeros((p, p));
for (k, &ev) in eigenvalues.iter().enumerate() {
if ev <= tol {
continue;
}
let inv_ev = 1.0 / ev;
for i in 0..p {
let vi = eigenvectors[[i, k]];
for j in 0..p {
pinv[[i, j]] += inv_ev * vi * eigenvectors[[j, k]];
}
}
}
symmetrize_dense_in_place(&mut pinv);
Ok(pinv)
}
pub(crate) fn symmetric_psd_projection(matrix: &Array2<f64>) -> Array2<f64> {
let p = matrix.nrows();
let mut sym = matrix.clone();
symmetrize_dense_in_place(&mut sym);
let Ok((evals, evecs)) = FaerEigh::eigh(&sym, Side::Lower) else {
return Array2::zeros((p, p));
};
if evals.iter().all(|lam| *lam >= 0.0) {
return sym;
}
let clamped = Array1::from_iter(evals.iter().map(|lam| lam.max(0.0)));
let scaled = &evecs * &clamped.view().insert_axis(ndarray::Axis(0));
scaled.dot(&evecs.t())
}
pub(crate) fn symmetric_negative_curvature_reflected(matrix: &Array2<f64>) -> Array2<f64> {
let p = matrix.nrows();
let mut sym = matrix.clone();
symmetrize_dense_in_place(&mut sym);
let Ok((evals, evecs)) = FaerEigh::eigh(&sym, Side::Lower) else {
return sym;
};
let lambda_max_abs = evals.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
if !(lambda_max_abs.is_finite() && lambda_max_abs > 0.0) {
return sym;
}
let floor = lambda_max_abs * (p as f64).sqrt() * f64::EPSILON;
let reflected = Array1::from_iter(evals.iter().map(|lam| lam.abs().max(floor)));
let scaled = &evecs * &reflected.view().insert_axis(ndarray::Axis(0));
scaled.dot(&evecs.t())
}
pub(crate) fn symmetric_min_eigenvalue_signed(matrix: &Array2<f64>) -> f64 {
let mut sym = matrix.clone();
symmetrize_dense_in_place(&mut sym);
match FaerEigh::eigh(&sym, Side::Lower) {
Ok((evals, _)) => evals.iter().copied().fold(f64::INFINITY, f64::min),
Err(_) => f64::NAN,
}
}
pub(crate) fn symmetric_penalized_hessian_nullity(lhs: &Array2<f64>) -> Option<usize> {
let p = lhs.nrows();
if p == 0 || lhs.ncols() != p {
return Some(0);
}
let (evals, _) = FaerEigh::eigh(lhs, Side::Lower).ok()?;
let max_abs = evals.iter().map(|x: &f64| x.abs()).fold(0.0_f64, f64::max);
if !(max_abs.is_finite() && max_abs > 0.0) {
return None;
}
let cutoff = KKT_REFUSAL_RANK_TOL * max_abs;
Some(evals.iter().filter(|x| x.abs() < cutoff).count())
}
pub(crate) fn symmetric_penalized_hessian_nullity_and_condition(
lhs: &Array2<f64>,
) -> Option<(usize, f64)> {
let p = lhs.nrows();
if p == 0 || lhs.ncols() != p {
return Some((0, 1.0));
}
let (evals, _) = FaerEigh::eigh(lhs, Side::Lower).ok()?;
let max_abs = evals.iter().map(|x: &f64| x.abs()).fold(0.0_f64, f64::max);
if !(max_abs.is_finite() && max_abs > 0.0) {
return None;
}
let cutoff = KKT_REFUSAL_RANK_TOL * max_abs;
let nullity = evals.iter().filter(|x| x.abs() < cutoff).count();
let min_range = evals
.iter()
.map(|x: &f64| x.abs())
.filter(|&m| m >= cutoff && m > 0.0)
.fold(f64::INFINITY, f64::min);
let condition = if min_range.is_finite() && min_range > 0.0 {
max_abs / min_range
} else {
f64::INFINITY
};
Some((nullity, condition))
}