use super::inner_strategy::GeometryBackendKind;
use super::penalty_logdet::PenaltyPseudologdet;
use super::*;
use crate::linalg::utils::enforce_symmetry;
const AUTO_CUBATURE_HESSIAN_RIDGE_REL: f64 = 1e-8;
const AUTO_CUBATURE_HESSIAN_RIDGE_ABS: f64 = 1e-8;
const AUTO_CUBATURE_RHO_CLAMP_INSET: f64 = 1e-8;
const AUTO_CUBATURE_RHOVAR_TRIGGER: f64 = 0.1;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SmoothingCorrectionFallbackSeverity {
Routine,
NumericalFailure,
}
#[derive(Clone, Debug)]
pub enum SmoothingCorrectionOutcome {
Cubature {
correction: Array2<f64>,
rank: usize,
n_points: usize,
near_boundary: bool,
grad_norm: f64,
max_rho_var: f64,
},
FirstOrder {
correction: Option<Array2<f64>>,
reason: &'static str,
severity: SmoothingCorrectionFallbackSeverity,
},
}
impl SmoothingCorrectionOutcome {
pub fn into_correction(self) -> Option<Array2<f64>> {
match self {
SmoothingCorrectionOutcome::Cubature { correction, .. } => Some(correction),
SmoothingCorrectionOutcome::FirstOrder { correction, .. } => correction,
}
}
pub fn branch_label(&self) -> &'static str {
match self {
SmoothingCorrectionOutcome::Cubature { .. } => "cubature",
SmoothingCorrectionOutcome::FirstOrder { severity, .. } => match severity {
SmoothingCorrectionFallbackSeverity::Routine => "first-order (routine)",
SmoothingCorrectionFallbackSeverity::NumericalFailure => {
"first-order (numerical failure)"
}
},
}
}
}
pub static SMOOTHING_CORRECTION_NUMERICAL_FAILURE_COUNT: AtomicU64 = AtomicU64::new(0);
pub(crate) type SigmaPointResult = Option<(Array2<f64>, Array1<f64>)>;
#[inline]
fn device_pirls_stage3_ready() -> bool {
crate::solver::gpu::cuda_selected() && crate::gpu::runtime::GpuRuntime::global().is_some()
}
pub(crate) fn sigma_cubature_dispatch(
state: &RemlState<'_>,
sigma_points: &[Array1<f64>],
) -> Vec<SigmaPointResult> {
if device_pirls_stage3_ready() {
match sigma_cubature_evaluate_gpu_stream_pool(state, sigma_points) {
Ok(Some(results)) => return results,
Ok(None) => {
log::debug!(
"[sigma-cubature] GPU stream pool declined (Ok(None)) — \
falling through to CPU Rayon oracle"
);
}
Err(e) => {
log::warn!(
"[sigma-cubature] GPU stream pool error — \
falling through to CPU Rayon oracle: {e:?}"
);
}
}
}
sigma_cubature_evaluate_cpu_rayon(state, sigma_points)
}
fn sigma_cubature_evaluate_gpu_stream_pool(
state: &RemlState<'_>,
sigma_points: &[Array1<f64>],
) -> Result<Option<Vec<SigmaPointResult>>, crate::gpu::GpuError> {
use crate::construction::{EngineDims, stable_reparameterization_engine_canonical};
use crate::gpu::runtime::GpuRuntime;
use crate::gpu::sigma_cubature::try_gpu_sigma_stream_pool_eval;
use crate::solver::gpu::pirls_dispatch::admission_for;
if sigma_points.is_empty() {
return Ok(Some(Vec::new()));
}
let n = state.x.nrows();
let p = state.p;
let x_dense = match state.x.as_dense() {
Some(d) => d,
None => return Ok(None),
};
let likelihood_spec = &state.config.likelihood;
let Some(admission) = admission_for(&likelihood_spec.spec, n, p) else {
return Ok(None);
};
let Some(runtime) = GpuRuntime::global() else {
return Ok(None);
};
if !runtime.policy().should_use_gpu_pirls_loop(admission) {
return Ok(None);
}
let engine_dims = EngineDims::new(p, state.canonical_penalties.len());
let mut per_sigma: Vec<crate::gpu::sigma_cubature::SigmaPointGpuInput> =
Vec::with_capacity(sigma_points.len());
for rho in sigma_points {
let lambdas = rho.mapv(f64::exp);
let lambdas_slice = lambdas
.as_slice_memory_order()
.ok_or_else(|| crate::gpu_err!("sigma rho lambdas not contiguous"))?;
let reparam = stable_reparameterization_engine_canonical(
&state.canonical_penalties,
lambdas_slice,
engine_dims,
Some(&state.reparam_invariant),
state.penalty_shrinkage_floor,
)
.map_err(|e| crate::gpu_err!("sigma reparam engine: {e:?}"))?;
let linear_shift = ndarray::Array1::<f64>::zeros(p);
per_sigma.push(crate::gpu::sigma_cubature::SigmaPointGpuInput {
s_transformed: reparam.s_transformed,
qs: reparam.qs,
linear_shift,
constant_shift: 0.0,
});
}
let gamma_shape = likelihood_spec.gamma_shape().unwrap_or(1.0);
try_gpu_sigma_stream_pool_eval(
x_dense.view(),
state.y,
state.weights,
state.offset.view(),
&per_sigma,
admission,
gamma_shape,
state.config.pirls_convergence_tolerance,
state.config.max_iterations,
)
}
fn sigma_cubature_evaluate_cpu_rayon(
state: &RemlState<'_>,
sigma_points: &[Array1<f64>],
) -> Vec<SigmaPointResult> {
(0..sigma_points.len())
.into_par_iter()
.map(|idx| {
let fit_point = state
.execute_pirls_stateless_for_cubature(&sigma_points[idx])
.ok()?;
let h_point = map_hessian_to_original_basis(fit_point.as_ref()).ok()?;
let cov_point = matrix_inversewith_regularization(&h_point, "auto cubature point")?;
let beta_point = fit_point
.reparam_result
.qs
.dot(fit_point.beta_transformed.as_ref());
Some((cov_point, beta_point))
})
.collect()
}
pub(crate) fn accumulate_sigma_cubature_total_covariance(
points: &[(Array2<f64>, Array1<f64>)],
p: usize,
) -> Array2<f64> {
let w = 1.0 / (points.len() as f64);
let mut mean_hinv = Array2::<f64>::zeros((p, p));
let mut mean_beta = Array1::<f64>::zeros(p);
let mut second_beta = Array2::<f64>::zeros((p, p));
for (cov_point, beta_point) in points {
mean_hinv.scaled_add(w, cov_point);
mean_beta.scaled_add(w, beta_point);
let beta_col = beta_point.view().insert_axis(ndarray::Axis(1));
let beta_row = beta_point.view().insert_axis(ndarray::Axis(0));
let outer = beta_col.dot(&beta_row);
second_beta.scaled_add(w, &outer);
}
let mean_outer = mean_beta
.view()
.insert_axis(ndarray::Axis(1))
.dot(&mean_beta.view().insert_axis(ndarray::Axis(0)));
let var_beta = second_beta - mean_outer;
mean_hinv + var_beta
}
pub static SMOOTHING_CORRECTION_CUBATURE_COUNT: AtomicU64 = AtomicU64::new(0);
impl<'a> RemlState<'a> {
pub(super) fn structural_penalty_logdet_derivatives(
&self,
rs_transformed: &[Array2<f64>],
lambdas: &Array1<f64>,
ridge: f64,
) -> Result<(Array1<f64>, Array2<f64>), EstimationError> {
let k_count = lambdas.len();
if rs_transformed.len() != k_count {
return Err(EstimationError::LayoutError(format!(
"Penalty root/lambda count mismatch in structural logdet derivatives: roots={}, lambdas={}",
rs_transformed.len(),
k_count
)));
}
if k_count == 0 {
return Ok((Array1::zeros(k_count), Array2::zeros((k_count, k_count))));
}
let s_k_matrices: Vec<Array2<f64>> = rs_transformed
.iter()
.map(|r_k| crate::faer_ndarray::fast_atb(r_k, r_k))
.collect();
let lambdas_slice = lambdas.as_slice().unwrap();
let pld = PenaltyPseudologdet::from_components(&s_k_matrices, lambdas_slice, ridge)
.map_err(EstimationError::LayoutError)?;
let (det1, det2) = pld.rho_derivatives(&s_k_matrices, lambdas_slice);
Ok((det1, det2))
}
pub(super) fn structural_penalty_logdet_derivatives_block_local(
&self,
lambdas: &Array1<f64>,
ridge: f64,
) -> Result<(Array1<f64>, Array2<f64>), EstimationError> {
if let Some(ref kron) = self.kronecker_penalty_system {
let lambdas_slice = lambdas.as_slice().unwrap();
let (_, det1, det2) = kron.logdet_and_derivatives(lambdas_slice, ridge);
return Ok((det1, det2));
}
let k_count = self.canonical_penalties.len();
if k_count == 0 || lambdas.len() != k_count {
return Ok((Array1::zeros(k_count), Array2::zeros((k_count, k_count))));
}
let lambdas_slice = lambdas.as_slice().unwrap();
let pld = PenaltyPseudologdet::from_penalties(
&self.canonical_penalties,
lambdas_slice,
ridge,
self.p,
)
.map_err(EstimationError::LayoutError)?;
let (det1, det2) =
pld.rho_derivatives_from_penalties(&self.canonical_penalties, lambdas_slice);
Ok((det1, det2))
}
pub(super) fn compute_lamlhessian_exact_from_bundle(
&self,
rho: &Array1<f64>,
bundle: &EvalShared,
) -> Result<Array2<f64>, EstimationError> {
let mode = super::unified::EvalMode::ValueGradientHessian;
let result = if bundle.backend_kind() == GeometryBackendKind::SparseExactSpd {
self.evaluate_unified_sparse(rho, bundle, mode)?
} else {
self.evaluate_unified(rho, bundle, mode)?
};
result
.hessian
.materialize_dense()
.map_err(EstimationError::RemlOptimizationFailed)?
.ok_or_else(|| {
EstimationError::RemlOptimizationFailed(
"Unified Hessian returned no analytic representation for VGH mode".into(),
)
})
}
pub(crate) fn compute_lamlhessian_consistent(
&self,
rho: &Array1<f64>,
) -> Result<Array2<f64>, EstimationError> {
let bundle = self.obtain_eval_bundle(rho)?;
let decision = self.selecthessian_strategy_policy(&bundle);
match decision.strategy {
super::inner_strategy::HessianEvalStrategyKind::SpectralExact => {
self.compute_lamlhessian_exact_from_bundle(rho, &bundle)
}
}
}
pub(crate) fn compute_smoothing_correction_auto(
&self,
final_rho: &Array1<f64>,
final_fit: &PirlsResult,
base_covariance: Option<&Array2<f64>>,
finalgrad_norm: f64,
) -> SmoothingCorrectionOutcome {
use SmoothingCorrectionFallbackSeverity::{NumericalFailure, Routine};
let first_order_routine = |correction: Option<Array2<f64>>, reason: &'static str| {
SmoothingCorrectionOutcome::FirstOrder {
correction,
reason,
severity: Routine,
}
};
let first_order_numerical = |correction: Option<Array2<f64>>, reason: &'static str| {
SmoothingCorrectionOutcome::FirstOrder {
correction,
reason,
severity: NumericalFailure,
}
};
let first_order = super::compute_smoothing_correction(self, final_rho, final_fit);
let first_order_correction = first_order.correction.clone();
let n_rho = final_rho.len();
if n_rho == 0 {
if let Some(base_cov) = base_covariance
&& let Ok(hop) = super::unified::DenseSpectralOperator::from_symmetric(base_cov)
{
let outer = Array2::<f64>::zeros((0, 0));
let unified_diag =
super::unified::compute_corrected_covariance_diagonal(&[], &[], &outer, &hop);
if let Ok(diag) = unified_diag {
let p = base_cov.nrows();
let max_dev = (0..p)
.map(|i| (base_cov[[i, i]] - diag[i]).abs())
.fold(0.0_f64, f64::max);
log::trace!(
"[corrected-cov] unified diagonal validation: max_dev={:.4e}",
max_dev,
);
}
let unified_full =
super::unified::compute_corrected_covariance(&[], &[], &outer, &hop);
if let Ok(full) = unified_full {
log::trace!(
"[corrected-cov] unified full norm: {:.4e}",
full.iter().map(|v| v * v).sum::<f64>().sqrt(),
);
}
}
return self.finalize_smoothing_outcome(first_order_routine(
first_order_correction,
"n_rho == 0: unified corrected covariance equals H^{-1}",
));
}
if n_rho > AUTO_CUBATURE_MAX_RHO_DIM {
return self.finalize_smoothing_outcome(first_order_routine(
first_order_correction,
"n_rho exceeds AUTO_CUBATURE_MAX_RHO_DIM: cubature cost prohibitive",
));
}
if final_fit.beta_transformed.len() > AUTO_CUBATURE_MAX_BETA_DIM {
return self.finalize_smoothing_outcome(first_order_routine(
first_order_correction,
"beta dimension exceeds AUTO_CUBATURE_MAX_BETA_DIM: cubature cost prohibitive",
));
}
let near_boundary = final_rho
.iter()
.any(|&v| (RHO_BOUND - v.abs()) <= AUTO_CUBATURE_BOUNDARY_MARGIN);
let grad_norm = if finalgrad_norm.is_finite() {
finalgrad_norm
} else {
0.0
};
const HIGHGRAD_REL_TOL: f64 = 1e-3;
let cost_scale = 1.0 + final_fit.deviance.abs();
let highgrad = grad_norm > HIGHGRAD_REL_TOL * cost_scale;
if !near_boundary && !highgrad {
return self.finalize_smoothing_outcome(first_order_routine(
first_order_correction,
"linearization sufficient: not near boundary and outer gradient is small",
));
}
if let Some(rank) = first_order.active_rank
&& rank < n_rho
{
return self.finalize_smoothing_outcome(first_order_routine(
first_order_correction,
"first-order V_rho rank-deficient: cubature would impute spurious variance",
));
}
let mut hessian_rho = if let Some(h) = first_order.hessian_rho {
h
} else {
match self.compute_lamlhessian_consistent(final_rho) {
Ok(h) => h,
Err(_) => {
return self.finalize_smoothing_outcome(first_order_numerical(
first_order_correction,
"rho Hessian compute_lamlhessian_consistent failed",
));
}
}
};
enforce_symmetry(&mut hessian_rho);
let ridge = AUTO_CUBATURE_HESSIAN_RIDGE_REL
* hessian_rho
.diag()
.iter()
.map(|&v| v.abs())
.fold(0.0, f64::max)
.max(AUTO_CUBATURE_HESSIAN_RIDGE_ABS);
for i in 0..n_rho {
hessian_rho[[i, i]] += ridge;
}
let Some(hessian_rho_inv) =
matrix_inversewith_regularization(&hessian_rho, "auto cubature rho Hessian")
else {
return self.finalize_smoothing_outcome(first_order_numerical(
first_order_correction,
"rho Hessian inversion failed after ridge regularization",
));
};
let max_rhovar = hessian_rho_inv
.diag()
.iter()
.fold(0.0_f64, |acc, &v| acc.max(v.abs()));
if !near_boundary && !highgrad && max_rhovar < AUTO_CUBATURE_RHOVAR_TRIGGER {
return self.finalize_smoothing_outcome(first_order_routine(
first_order_correction,
"post-inversion rho posterior variance below trigger threshold",
));
}
use crate::faer_ndarray::FaerEigh;
use faer::Side;
let (evals, evecs) = match hessian_rho_inv.eigh(Side::Lower) {
Ok(x) => x,
Err(_) => {
return self.finalize_smoothing_outcome(first_order_numerical(
first_order_correction,
"eigendecomposition of inverse rho-Hessian failed",
));
}
};
let max_eval = evals
.iter()
.copied()
.fold(0.0_f64, |acc, value| acc.max(value.abs()));
let eigenvalue_floor = max_eval * (n_rho.max(1) as f64) * f64::EPSILON;
let mut eig_pairs: Vec<(usize, f64)> = evals
.iter()
.copied()
.enumerate()
.filter(|(_, v)| v.is_finite() && *v > eigenvalue_floor)
.collect();
if eig_pairs.is_empty() {
return self.finalize_smoothing_outcome(first_order_numerical(
first_order_correction,
"inverse rho-Hessian has no positive eigenvalues above numerical floor",
));
}
eig_pairs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let totalvar: f64 = eig_pairs.iter().map(|(_, v)| *v).sum();
if !totalvar.is_finite() || totalvar <= 0.0 {
return self.finalize_smoothing_outcome(first_order_numerical(
first_order_correction,
"positive-eigenvalue total mass non-finite or non-positive",
));
}
let mut rank = 0usize;
let mut captured = 0.0_f64;
for (_, eig) in eig_pairs
.iter()
.take(AUTO_CUBATURE_MAX_EIGENVECTORS.min(eig_pairs.len()))
{
captured += *eig;
rank += 1;
if captured / totalvar >= AUTO_CUBATURE_TARGET_VAR_FRAC {
break;
}
}
if rank == 0 {
return self.finalize_smoothing_outcome(first_order_numerical(
first_order_correction,
"variance-truncation produced rank 0 (unreachable guard)",
));
}
let Some(base_cov) = base_covariance else {
return self.finalize_smoothing_outcome(first_order_routine(
first_order_correction,
"no base covariance supplied: nothing for cubature to upgrade",
));
};
let p = base_cov.nrows();
let radius = (rank as f64).sqrt();
let mut sigma_points: Vec<Array1<f64>> = Vec::with_capacity(2 * rank);
for (eig_idx, eigval) in eig_pairs.iter().take(rank) {
let axis = evecs.column(*eig_idx).to_owned();
let scale = radius * eigval.sqrt();
let delta = axis.mapv(|v| v * scale);
let lo = -RHO_BOUND + AUTO_CUBATURE_RHO_CLAMP_INSET;
let hi = RHO_BOUND - AUTO_CUBATURE_RHO_CLAMP_INSET;
for sign in [1.0_f64, -1.0_f64] {
let mut rho_point = final_rho.clone();
rho_point
.iter_mut()
.zip(delta.iter())
.for_each(|(r, &d)| *r = (*r + sign * d).clamp(lo, hi));
sigma_points.push(rho_point);
}
}
if sigma_points.is_empty() {
return self.finalize_smoothing_outcome(first_order_numerical(
first_order_correction,
"empty sigma-point set (unreachable guard)",
));
}
let point_results = sigma_cubature_dispatch(self, &sigma_points);
if point_results.iter().any(|r| r.is_none()) {
return self.finalize_smoothing_outcome(first_order_numerical(
first_order_correction,
"one or more sigma-point inner PIRLS fits failed",
));
}
let point_pairs: Vec<(Array2<f64>, Array1<f64>)> =
point_results.into_iter().flatten().collect();
let mut total_cov = accumulate_sigma_cubature_total_covariance(&point_pairs, p);
if !total_cov.iter().all(|v| v.is_finite()) {
return self.finalize_smoothing_outcome(first_order_numerical(
first_order_correction,
"assembled total covariance contains non-finite entries",
));
}
enforce_symmetry(&mut total_cov);
let mut corr = total_cov - base_cov;
enforce_symmetry(&mut corr);
self.finalize_smoothing_outcome(SmoothingCorrectionOutcome::Cubature {
correction: corr,
rank,
n_points: sigma_points.len(),
near_boundary,
grad_norm,
max_rho_var: max_rhovar,
})
}
fn finalize_smoothing_outcome(
&self,
outcome: SmoothingCorrectionOutcome,
) -> SmoothingCorrectionOutcome {
let branch_label = outcome.branch_label();
match &outcome {
SmoothingCorrectionOutcome::Cubature {
rank,
n_points,
near_boundary,
grad_norm,
max_rho_var,
..
} => {
SMOOTHING_CORRECTION_CUBATURE_COUNT.fetch_add(1, Ordering::Relaxed);
log::info!(
"[smoothing-correction] branch={} rank={} points={} near_boundary={} \
grad_norm={:.3e} max_rho_var={:.3e}",
branch_label,
rank,
n_points,
near_boundary,
grad_norm,
max_rho_var,
);
}
SmoothingCorrectionOutcome::FirstOrder {
reason,
severity,
correction,
} => {
let has_matrix = correction.is_some();
match severity {
SmoothingCorrectionFallbackSeverity::Routine => {
log::info!(
"[smoothing-correction] branch=first-order severity=routine \
has_matrix={} reason=\"{}\"",
has_matrix,
reason
);
}
SmoothingCorrectionFallbackSeverity::NumericalFailure => {
SMOOTHING_CORRECTION_NUMERICAL_FAILURE_COUNT
.fetch_add(1, Ordering::Relaxed);
log::warn!(
"[smoothing-correction] branch=first-order severity=numerical-failure \
has_matrix={} reason=\"{}\" failure_count={}",
has_matrix,
reason,
SMOOTHING_CORRECTION_NUMERICAL_FAILURE_COUNT.load(Ordering::Relaxed),
);
}
}
}
}
outcome
}
}
#[cfg(test)]
mod sigma_cubature_accumulation_tests {
use super::accumulate_sigma_cubature_total_covariance;
use ndarray::{Array1, Array2};
#[test]
fn cubature_linear_exactness_recovers_jvjt() {
let p = 4;
let d_rho = 3;
let r = 3;
let m_points = 2 * r;
let eigenvalues = [0.25_f64, 0.49, 0.81];
let u: Array2<f64> = ndarray::array![
[1.0 / 3f64.sqrt(), 1.0 / 2f64.sqrt(), 1.0 / 6f64.sqrt()],
[1.0 / 3f64.sqrt(), -1.0 / 2f64.sqrt(), 1.0 / 6f64.sqrt()],
[1.0 / 3f64.sqrt(), 0.0, -2.0 / 6f64.sqrt()],
];
let ut_u = u.t().dot(&u);
for i in 0..d_rho {
for j in 0..d_rho {
let want = if i == j { 1.0 } else { 0.0 };
assert!(
(ut_u[[i, j]] - want).abs() < 1e-12,
"U is not orthonormal at ({i},{j}): got {} expected {}",
ut_u[[i, j]],
want,
);
}
}
let mut v_rho_r = Array2::<f64>::zeros((d_rho, d_rho));
for k in 0..d_rho {
let col = u.column(k);
let scaled = col.mapv(|v| v * eigenvalues[k]);
for i in 0..d_rho {
for j in 0..d_rho {
v_rho_r[[i, j]] += scaled[i] * col[j];
}
}
}
let mut sigma_displacements: Vec<Array1<f64>> = Vec::with_capacity(m_points);
for k in 0..r {
let scale = (r as f64 * eigenvalues[k]).sqrt();
let axis = u.column(k).to_owned();
for sign in [1.0_f64, -1.0_f64] {
sigma_displacements.push(axis.mapv(|v| v * sign * scale));
}
}
let b0: Array1<f64> = ndarray::array![1.0, -2.0, 3.5, 0.5];
let jacobian: Array2<f64> = ndarray::array![
[1.0, 0.0, -1.0],
[2.0, 1.0, 0.0],
[0.0, -1.0, 1.0],
[1.0, 1.0, 1.0],
];
let mut a0 = Array2::<f64>::eye(p);
a0[[0, 1]] = 0.25;
a0[[1, 0]] = 0.25;
a0[[2, 3]] = -0.10;
a0[[3, 2]] = -0.10;
let points: Vec<(Array2<f64>, Array1<f64>)> = sigma_displacements
.iter()
.map(|drho| {
let bm = &b0 + &jacobian.dot(drho);
(a0.clone(), bm)
})
.collect();
let jvjt = jacobian.dot(&v_rho_r).dot(&jacobian.t());
let expected = &a0 + &jvjt;
let actual = accumulate_sigma_cubature_total_covariance(&points, p);
let mut max_rel_dev = 0.0_f64;
let mut max_abs_dev = 0.0_f64;
for i in 0..p {
for j in 0..p {
let diff = (actual[[i, j]] - expected[[i, j]]).abs();
let denom = expected[[i, j]].abs().max(1.0);
max_rel_dev = max_rel_dev.max(diff / denom);
max_abs_dev = max_abs_dev.max(diff);
}
}
assert!(
max_rel_dev < 1e-12,
"cubature linear-exactness violation: max_rel_dev={:.3e}, max_abs_dev={:.3e}",
max_rel_dev,
max_abs_dev,
);
}
#[test]
fn cubature_single_point_collapses_to_a0() {
let p = 3;
let a0: Array2<f64> = ndarray::array![[2.0, 0.5, 0.0], [0.5, 1.5, 0.25], [0.0, 0.25, 1.0]];
let b0: Array1<f64> = ndarray::array![0.1, -0.2, 0.3];
let points = vec![(a0.clone(), b0.clone())];
let actual = accumulate_sigma_cubature_total_covariance(&points, p);
for i in 0..p {
for j in 0..p {
let diff = (actual[[i, j]] - a0[[i, j]]).abs();
assert!(
diff < 1e-14,
"single-point cubature did not collapse to A_0 at ({i},{j}): \
actual={}, expected={}, diff={:.3e}",
actual[[i, j]],
a0[[i, j]],
diff,
);
}
}
}
#[test]
fn cubature_antipodal_pairing_annihilates_linear_drift() {
let p = 3;
let r = 3;
let m = 2 * r;
let scales = [0.7_f64, 1.3, 0.4];
let mut displacements: Vec<Array1<f64>> = Vec::with_capacity(m);
for k in 0..r {
for sign in [1.0_f64, -1.0_f64] {
let mut d = Array1::<f64>::zeros(r);
d[k] = sign * scales[k];
displacements.push(d);
}
}
let b0: Array1<f64> = ndarray::array![2.5, -1.25, 4.0];
let j: Array2<f64> =
ndarray::array![[1.0, -2.0, 0.5], [0.0, 1.5, -1.0], [-0.75, 0.25, 2.0],];
let a0: Array2<f64> =
ndarray::array![[3.0, 0.5, -0.25], [0.5, 2.0, 0.10], [-0.25, 0.10, 1.5],];
let points: Vec<(Array2<f64>, Array1<f64>)> = displacements
.iter()
.map(|drho| (a0.clone(), &b0 + &j.dot(drho)))
.collect();
let actual = accumulate_sigma_cubature_total_covariance(&points, p);
let w = 1.0 / (m as f64);
let mut v_rho = Array2::<f64>::zeros((r, r));
for d in &displacements {
for i in 0..r {
for jj in 0..r {
v_rho[[i, jj]] += w * d[i] * d[jj];
}
}
}
let jvjt = j.dot(&v_rho).dot(&j.t());
let expected = &a0 + &jvjt;
let mut max_rel_dev = 0.0_f64;
let mut max_abs_dev = 0.0_f64;
for i in 0..p {
for jj in 0..p {
let diff = (actual[[i, jj]] - expected[[i, jj]]).abs();
let denom = expected[[i, jj]].abs().max(1.0);
max_rel_dev = max_rel_dev.max(diff / denom);
max_abs_dev = max_abs_dev.max(diff);
}
}
assert!(
max_rel_dev < 1e-12,
"antipodal-pairing drift on linear b_m: max_rel_dev={:.3e}, \
max_abs_dev={:.3e}",
max_rel_dev,
max_abs_dev,
);
}
#[test]
fn cubature_constant_a_in_implies_constant_a_out_on_a_side() {
let p = 4;
let a0: Array2<f64> = ndarray::array![
[2.0, 0.30, 0.10, 0.05],
[0.30, 1.50, 0.20, -0.10],
[0.10, 0.20, 1.20, 0.15],
[0.05, -0.10, 0.15, 0.80],
];
let zero_b = Array1::<f64>::zeros(p);
for m in [1usize, 2, 4, 6, 8, 16] {
let points: Vec<(Array2<f64>, Array1<f64>)> =
(0..m).map(|_| (a0.clone(), zero_b.clone())).collect();
let actual = accumulate_sigma_cubature_total_covariance(&points, p);
for i in 0..p {
for j in 0..p {
let diff = (actual[[i, j]] - a0[[i, j]]).abs();
assert!(
diff < 1e-14,
"constant-A invariance violated at M={m}, ({i},{j}): \
actual={}, expected={}, diff={:.3e}",
actual[[i, j]],
a0[[i, j]],
diff,
);
}
}
}
}
#[test]
fn cubature_permutation_invariance_on_antipodal_pairs() {
let p = 4;
let r = 3;
let b0: Array1<f64> = ndarray::array![1.0, -2.0, 3.5, 0.5];
let j: Array2<f64> = ndarray::array![
[1.0, 0.0, -1.0],
[2.0, 1.0, 0.0],
[0.0, -1.0, 1.0],
[1.0, 1.0, 1.0],
];
let a_for_idx = |idx: usize| -> Array2<f64> {
let mut a = Array2::<f64>::eye(p);
for d in 0..p {
a[[d, d]] = 1.0 + 0.05 * (idx as f64 + 1.0);
}
a
};
let scales = [0.7_f64, 1.3, 0.4];
let mut interleaved: Vec<(Array2<f64>, Array1<f64>)> = Vec::with_capacity(2 * r);
for k in 0..r {
for sign in [1.0_f64, -1.0_f64] {
let mut d = Array1::<f64>::zeros(r);
d[k] = sign * scales[k];
let bm = &b0 + &j.dot(&d);
interleaved.push((a_for_idx(interleaved.len()), bm));
}
}
let mut permuted: Vec<(Array2<f64>, Array1<f64>)> = Vec::with_capacity(2 * r);
for k in 0..r {
permuted.push(interleaved[2 * k].clone()); }
for k in 0..r {
permuted.push(interleaved[2 * k + 1].clone()); }
let v_interleaved = accumulate_sigma_cubature_total_covariance(&interleaved, p);
let v_permuted = accumulate_sigma_cubature_total_covariance(&permuted, p);
let mut max_abs_dev = 0.0_f64;
for i in 0..p {
for jj in 0..p {
let diff = (v_interleaved[[i, jj]] - v_permuted[[i, jj]]).abs();
max_abs_dev = max_abs_dev.max(diff);
}
}
assert!(
max_abs_dev < 1e-13,
"permutation invariance violated: max_abs_dev={:.3e}",
max_abs_dev,
);
}
#[test]
fn cubature_dispatch_swap_site_invariant_holds_pre_gpu() {
let p = 3;
let a: Array2<f64> =
ndarray::array![[1.5, 0.20, 0.10], [0.20, 1.20, 0.05], [0.10, 0.05, 0.90],];
let b0: Array1<f64> = ndarray::array![0.30, -0.40, 0.10];
let mut points: Vec<(Array2<f64>, Array1<f64>)> = Vec::new();
for k in 0..3 {
for sign in [1.0_f64, -1.0_f64] {
let mut bm = b0.clone();
bm[k] += sign * 0.25;
points.push((a.clone(), bm));
}
}
let first = accumulate_sigma_cubature_total_covariance(&points, p);
let second = accumulate_sigma_cubature_total_covariance(&points, p);
for i in 0..p {
for j in 0..p {
assert_eq!(
first[[i, j]],
second[[i, j]],
"accumulator non-deterministic at ({i},{j}): \
first={} second={}",
first[[i, j]],
second[[i, j]],
);
}
}
}
#[test]
fn cubature_arbitrary_spd_a_in_obeys_total_covariance_law() {
let p = 3;
let mk_spd = |scale: f64, off: f64| -> Array2<f64> {
let m: Array2<f64> = ndarray::array![
[scale, off, 0.5 * off],
[-off, scale + 0.1, off],
[0.25 * off, -0.5 * off, scale - 0.1],
];
let mut a = m.dot(&m.t());
for i in 0..p {
a[[i, i]] += 1e-3;
}
a
};
let a0 = mk_spd(1.0, 0.20);
let a1 = mk_spd(1.3, 0.10);
let a2 = mk_spd(0.7, -0.15);
let b0: Array1<f64> = ndarray::array![0.1, 0.2, 0.3];
let b1: Array1<f64> = ndarray::array![-0.1, 0.4, -0.2];
let b2: Array1<f64> = ndarray::array![0.5, -0.3, 0.0];
let points = vec![
(a0.clone(), b0.clone()),
(a1.clone(), b1.clone()),
(a2.clone(), b2.clone()),
];
let w = 1.0 / 3.0;
let mut mean_a = Array2::<f64>::zeros((p, p));
mean_a.scaled_add(w, &a0);
mean_a.scaled_add(w, &a1);
mean_a.scaled_add(w, &a2);
let mut mean_b = Array1::<f64>::zeros(p);
mean_b.scaled_add(w, &b0);
mean_b.scaled_add(w, &b1);
mean_b.scaled_add(w, &b2);
let outer = |v: &Array1<f64>| -> Array2<f64> {
v.view()
.insert_axis(ndarray::Axis(1))
.dot(&v.view().insert_axis(ndarray::Axis(0)))
};
let mut second = Array2::<f64>::zeros((p, p));
second.scaled_add(w, &outer(&b0));
second.scaled_add(w, &outer(&b1));
second.scaled_add(w, &outer(&b2));
let expected = &mean_a + &(&second - &outer(&mean_b));
let actual = accumulate_sigma_cubature_total_covariance(&points, p);
let mut max_abs = 0.0_f64;
for i in 0..p {
for j in 0..p {
max_abs = max_abs.max((actual[[i, j]] - expected[[i, j]]).abs());
}
}
assert!(
max_abs < 1e-13,
"arbitrary-SPD A injection broke total-covariance law: max_abs={:.3e}",
max_abs,
);
}
#[test]
fn cubature_beta_scaling_propagates_quadratically() {
let p = 3;
let a0: Array2<f64> = ndarray::array![[2.0, 0.1, 0.0], [0.1, 1.5, 0.05], [0.0, 0.05, 1.0],];
let raw_betas: Vec<Array1<f64>> = vec![
ndarray::array![1.0, -0.5, 0.3],
ndarray::array![-1.0, 0.5, -0.3],
ndarray::array![0.7, 0.2, -0.1],
ndarray::array![-0.7, -0.2, 0.1],
];
let unscaled: Vec<(Array2<f64>, Array1<f64>)> =
raw_betas.iter().map(|b| (a0.clone(), b.clone())).collect();
let v_unscaled = accumulate_sigma_cubature_total_covariance(&unscaled, p);
let alpha = 2.5_f64;
let scaled: Vec<(Array2<f64>, Array1<f64>)> = raw_betas
.iter()
.map(|b| (a0.clone(), b.mapv(|x| x * alpha)))
.collect();
let v_scaled = accumulate_sigma_cubature_total_covariance(&scaled, p);
let mut max_rel = 0.0_f64;
for i in 0..p {
for j in 0..p {
let expected = a0[[i, j]] + alpha * alpha * (v_unscaled[[i, j]] - a0[[i, j]]);
let diff = (v_scaled[[i, j]] - expected).abs();
let denom = expected.abs().max(1.0);
max_rel = max_rel.max(diff / denom);
}
}
assert!(
max_rel < 1e-12,
"β-scaling quadratic propagation violated: max_rel={:.3e}",
max_rel,
);
}
#[test]
fn cubature_full_reversal_permutation_invariance() {
let p = 4;
let m = 8;
let mut points: Vec<(Array2<f64>, Array1<f64>)> = Vec::with_capacity(m);
for idx in 0..m {
let mut a = Array2::<f64>::eye(p);
for d in 0..p {
a[[d, d]] = 1.0 + 0.07 * (idx as f64);
}
let b: Array1<f64> = (0..p)
.map(|d| 0.1 + 0.3 * (idx as f64) - 0.05 * (d as f64))
.collect();
points.push((a, b));
}
let reversed: Vec<(Array2<f64>, Array1<f64>)> = points.iter().rev().cloned().collect();
let v_forward = accumulate_sigma_cubature_total_covariance(&points, p);
let v_reverse = accumulate_sigma_cubature_total_covariance(&reversed, p);
let mut max_abs = 0.0_f64;
for i in 0..p {
for j in 0..p {
max_abs = max_abs.max((v_forward[[i, j]] - v_reverse[[i, j]]).abs());
}
}
assert!(
max_abs < 1e-12,
"full-reversal permutation invariance violated: max_abs={:.3e}",
max_abs,
);
}
#[test]
fn cubature_m_doubling_leaves_output_unchanged() {
let p = 3;
let a_mk = |s: f64| -> Array2<f64> {
let mut a = Array2::<f64>::eye(p);
a[[0, 0]] = 1.0 + s;
a[[1, 1]] = 1.2 + 0.5 * s;
a[[2, 2]] = 0.8 + 0.3 * s;
a[[0, 1]] = 0.1 * s;
a[[1, 0]] = 0.1 * s;
a
};
let original: Vec<(Array2<f64>, Array1<f64>)> = (0..4)
.map(|i| {
let s = (i as f64) / 4.0;
let b: Array1<f64> = ndarray::array![s, -s, 0.5 * s];
(a_mk(s), b)
})
.collect();
let mut doubled: Vec<(Array2<f64>, Array1<f64>)> = Vec::with_capacity(2 * original.len());
for pt in &original {
doubled.push(pt.clone());
doubled.push(pt.clone());
}
let v_orig = accumulate_sigma_cubature_total_covariance(&original, p);
let v_doub = accumulate_sigma_cubature_total_covariance(&doubled, p);
let mut max_abs = 0.0_f64;
for i in 0..p {
for j in 0..p {
max_abs = max_abs.max((v_orig[[i, j]] - v_doub[[i, j]]).abs());
}
}
assert!(
max_abs < 1e-13,
"M-doubling changed the cubature output: max_abs={:.3e}",
max_abs,
);
}
#[test]
fn cubature_rank_deficient_v_rho_collapses_var_to_zero() {
let p = 3;
let b_const: Array1<f64> = ndarray::array![0.7, -0.2, 0.4];
let a_list = [
ndarray::array![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
ndarray::array![[2.0, 0.0, 0.0], [0.0, 1.5, 0.0], [0.0, 0.0, 0.8]],
ndarray::array![[1.2, 0.1, 0.0], [0.1, 1.3, 0.0], [0.0, 0.0, 0.9]],
];
let points: Vec<(Array2<f64>, Array1<f64>)> = a_list
.iter()
.map(|a| (a.clone(), b_const.clone()))
.collect();
let actual = accumulate_sigma_cubature_total_covariance(&points, p);
let w = 1.0 / (a_list.len() as f64);
let mut mean_a = Array2::<f64>::zeros((p, p));
for a in &a_list {
mean_a.scaled_add(w, a);
}
let mut max_abs = 0.0_f64;
for i in 0..p {
for j in 0..p {
max_abs = max_abs.max((actual[[i, j]] - mean_a[[i, j]]).abs());
}
}
assert!(
max_abs < 1e-13,
"rank-deficient V_ρ did not collapse var(b) to zero: \
max_abs={:.3e}",
max_abs,
);
}
#[test]
fn sigma_loop_v100_hill_climb_baseline() {
let p = 50_usize;
let m = 8_usize;
let points: Vec<(Array2<f64>, Array1<f64>)> = (0..m)
.map(|idx| {
let mut lower = Array2::<f64>::zeros((p, p));
for i in 0..p {
for j in 0..=i {
let off = (i as f64 + 1.0) * (j as f64 + 1.0) + 0.1 * (idx as f64);
lower[[i, j]] = (off.sin()) * 0.05 + if i == j { 1.0 } else { 0.0 };
}
}
let mut a = lower.dot(&lower.t());
for d in 0..p {
a[[d, d]] += 1e-3;
}
let b: Array1<f64> = (0..p)
.map(|d| {
let phase = (d as f64 + 1.0) * 0.13 + (idx as f64) * 0.27;
phase.cos() * 0.3 - phase.sin() * 0.1
})
.collect();
(a, b)
})
.collect();
let reps = 20_usize;
let t0 = std::time::Instant::now();
let mut last_trace = 0.0_f64;
for _ in 0..reps {
let v = accumulate_sigma_cubature_total_covariance(&points, p);
let mut tr = 0.0_f64;
for d in 0..p {
tr += v[[d, d]];
}
last_trace += tr;
}
let elapsed = t0.elapsed();
let per_call_us = elapsed.as_secs_f64() * 1e6 / reps as f64;
assert!(
per_call_us < 10_000.0,
"sigma cubature accumulator baseline regressed: \
per-call {:.1} µs at (p={p}, M={m}) — expected < 10 ms",
per_call_us,
);
assert!(
last_trace.is_finite(),
"accumulator produced non-finite trace sum: {last_trace}"
);
let stage3_ready = super::device_pirls_stage3_ready();
log::info!(
"[sigma-hill-climb] accumulator baseline: \
per-call={per_call_us:.1}µs (p={p}, M={m}, reps={reps}); \
stage3_ready={stage3_ready}; 5× target gates on \
stage3_ready=true"
);
}
#[test]
fn cubature_a_side_is_convex_on_input() {
let p = 3;
let r = 2;
let m = 2 * r;
let b0: Array1<f64> = ndarray::array![0.4, -0.3, 0.2];
let scales = [0.6_f64, 1.1];
let points_b: Vec<Array1<f64>> = (0..r)
.flat_map(|k| {
let b_outer = b0.clone();
[1.0_f64, -1.0_f64].into_iter().map(move |sign| {
let mut b = b_outer.clone();
b[k] += sign * scales[k];
b
})
})
.collect();
let a_set_1: Vec<Array2<f64>> = (0..m)
.map(|i| {
let mut a = Array2::<f64>::eye(p);
a[[0, 0]] = 1.0 + 0.05 * i as f64;
a[[1, 1]] = 1.5 - 0.02 * i as f64;
a[[0, 1]] = 0.10;
a[[1, 0]] = 0.10;
a
})
.collect();
let a_set_2: Vec<Array2<f64>> = (0..m)
.map(|i| {
let mut a = Array2::<f64>::eye(p);
a[[0, 0]] = 2.0 - 0.04 * i as f64;
a[[2, 2]] = 0.9 + 0.03 * i as f64;
a[[1, 2]] = -0.05;
a[[2, 1]] = -0.05;
a
})
.collect();
let alpha = 0.3_f64;
let beta = 1.0 - alpha; let a_set_mix: Vec<Array2<f64>> = a_set_1
.iter()
.zip(a_set_2.iter())
.map(|(a, ap)| a.mapv(|v| v * alpha) + ap.mapv(|v| v * beta))
.collect();
let pts1: Vec<(Array2<f64>, Array1<f64>)> = a_set_1
.iter()
.zip(points_b.iter())
.map(|(a, b)| (a.clone(), b.clone()))
.collect();
let pts2: Vec<(Array2<f64>, Array1<f64>)> = a_set_2
.iter()
.zip(points_b.iter())
.map(|(a, b)| (a.clone(), b.clone()))
.collect();
let pts_mix: Vec<(Array2<f64>, Array1<f64>)> = a_set_mix
.iter()
.zip(points_b.iter())
.map(|(a, b)| (a.clone(), b.clone()))
.collect();
let v1 = accumulate_sigma_cubature_total_covariance(&pts1, p);
let v2 = accumulate_sigma_cubature_total_covariance(&pts2, p);
let vmix = accumulate_sigma_cubature_total_covariance(&pts_mix, p);
let expected = v1.mapv(|v| v * alpha) + v2.mapv(|v| v * beta);
let mut max_abs = 0.0_f64;
for i in 0..p {
for j in 0..p {
max_abs = max_abs.max((vmix[[i, j]] - expected[[i, j]]).abs());
}
}
assert!(
max_abs < 1e-13,
"A-side convexity violated: max_abs={:.3e}",
max_abs,
);
}
#[test]
fn cubature_b_translation_leaves_output_unchanged() {
let p = 3;
let a_const: Array2<f64> =
ndarray::array![[1.5, 0.2, 0.0], [0.2, 1.2, 0.1], [0.0, 0.1, 1.0],];
let raw_bs: Vec<Array1<f64>> = vec![
ndarray::array![0.4, -0.3, 0.2],
ndarray::array![-0.4, 0.3, -0.2],
ndarray::array![0.1, 0.5, -0.4],
ndarray::array![-0.1, -0.5, 0.4],
];
let pts_raw: Vec<(Array2<f64>, Array1<f64>)> = raw_bs
.iter()
.map(|b| (a_const.clone(), b.clone()))
.collect();
let shift: Array1<f64> = ndarray::array![10.0, -5.0, 3.0];
let pts_shifted: Vec<(Array2<f64>, Array1<f64>)> = raw_bs
.iter()
.map(|b| (a_const.clone(), b + &shift))
.collect();
let v_raw = accumulate_sigma_cubature_total_covariance(&pts_raw, p);
let v_shifted = accumulate_sigma_cubature_total_covariance(&pts_shifted, p);
let mut max_abs = 0.0_f64;
for i in 0..p {
for j in 0..p {
max_abs = max_abs.max((v_raw[[i, j]] - v_shifted[[i, j]]).abs());
}
}
assert!(
max_abs < 1e-11,
"b-translation invariance violated: max_abs={:.3e}",
max_abs,
);
}
#[test]
fn cubature_block_diagonal_inputs_yield_block_diagonal_output() {
let p_top = 2;
let p_bot = 2;
let p = p_top + p_bot;
let m = 4;
let a_top: Array2<f64> = ndarray::array![[1.0, 0.1], [0.1, 1.2]];
let a_bot: Array2<f64> = ndarray::array![[0.8, 0.05], [0.05, 0.7]];
let mut a_full = Array2::<f64>::zeros((p, p));
for i in 0..p_top {
for j in 0..p_top {
a_full[[i, j]] = a_top[[i, j]];
}
}
for i in 0..p_bot {
for j in 0..p_bot {
a_full[[p_top + i, p_top + j]] = a_bot[[i, j]];
}
}
let b_bot_const: Array1<f64> = ndarray::array![0.5, -0.3];
let top_bs: Vec<Array1<f64>> = vec![
ndarray::array![0.4, 0.1],
ndarray::array![-0.4, -0.1],
ndarray::array![0.2, 0.3],
ndarray::array![-0.2, -0.3],
];
let mut points: Vec<(Array2<f64>, Array1<f64>)> = Vec::with_capacity(m);
for top in &top_bs {
let mut b = Array1::<f64>::zeros(p);
for i in 0..p_top {
b[i] = top[i];
}
for i in 0..p_bot {
b[p_top + i] = b_bot_const[i];
}
points.push((a_full.clone(), b));
}
let v = accumulate_sigma_cubature_total_covariance(&points, p);
let mut max_cross_abs = 0.0_f64;
for i in 0..p_top {
for j in 0..p_bot {
max_cross_abs = max_cross_abs.max(v[[i, p_top + j]].abs());
max_cross_abs = max_cross_abs.max(v[[p_top + j, i]].abs());
}
}
assert!(
max_cross_abs < 1e-13,
"block-diagonal inputs leaked cross-block coupling: \
max_cross_abs={:.3e}",
max_cross_abs,
);
}
#[test]
fn cubature_output_is_symmetric_for_symmetric_inputs() {
let p = 5;
let m = 6;
let points: Vec<(Array2<f64>, Array1<f64>)> = (0..m)
.map(|idx| {
let mut a = Array2::<f64>::eye(p);
for i in 0..p {
for j in 0..i {
let v = 0.05 + 0.03 * (i as f64 + j as f64) + 0.02 * (idx as f64);
a[[i, j]] = v;
a[[j, i]] = v;
}
a[[i, i]] = 2.0 + 0.1 * idx as f64;
}
let b: Array1<f64> = (0..p)
.map(|d| 0.1 * (d as f64 + 1.0) + 0.05 * idx as f64)
.collect();
(a, b)
})
.collect();
let v = accumulate_sigma_cubature_total_covariance(&points, p);
let mut max_asym = 0.0_f64;
for i in 0..p {
for j in (i + 1)..p {
max_asym = max_asym.max((v[[i, j]] - v[[j, i]]).abs());
}
}
assert!(
max_asym < 1e-13,
"output not symmetric for symmetric inputs: max_asym={:.3e}",
max_asym,
);
}
#[test]
fn cubature_a_side_unchanged_under_b_permutation() {
let p = 3;
let m = 6;
let a_set: Vec<Array2<f64>> = (0..m)
.map(|i| {
let mut a = Array2::<f64>::eye(p);
a[[0, 0]] = 1.0 + 0.07 * (i as f64);
a[[1, 1]] = 1.5 - 0.05 * (i as f64);
a[[2, 2]] = 0.9 + 0.04 * (i as f64);
a[[0, 1]] = 0.05;
a[[1, 0]] = 0.05;
a
})
.collect();
let b_const: Array1<f64> = ndarray::array![0.2, -0.1, 0.3];
let original: Vec<(Array2<f64>, Array1<f64>)> =
a_set.iter().map(|a| (a.clone(), b_const.clone())).collect();
let perm: [usize; 6] = [3, 0, 5, 1, 4, 2];
let permuted_a: Vec<(Array2<f64>, Array1<f64>)> = perm
.iter()
.map(|&i| (a_set[i].clone(), b_const.clone()))
.collect();
let v_orig = accumulate_sigma_cubature_total_covariance(&original, p);
let v_perm = accumulate_sigma_cubature_total_covariance(&permuted_a, p);
let w = 1.0 / m as f64;
let mut mean_a = Array2::<f64>::zeros((p, p));
for a in &a_set {
mean_a.scaled_add(w, a);
}
let mut max_abs_orig = 0.0_f64;
let mut max_abs_perm = 0.0_f64;
for i in 0..p {
for j in 0..p {
max_abs_orig = max_abs_orig.max((v_orig[[i, j]] - mean_a[[i, j]]).abs());
max_abs_perm = max_abs_perm.max((v_perm[[i, j]] - mean_a[[i, j]]).abs());
}
}
assert!(
max_abs_orig < 1e-13 && max_abs_perm < 1e-13,
"A-side independent of A ordering under constant b: \
orig={:.3e}, perm={:.3e}",
max_abs_orig,
max_abs_perm,
);
}
}
#[cfg(test)]
mod smoothing_correction_outcome_tests {
use super::*;
use ndarray::array;
use std::sync::atomic::Ordering;
fn make_first_order(
reason: &'static str,
severity: SmoothingCorrectionFallbackSeverity,
with_matrix: bool,
) -> SmoothingCorrectionOutcome {
let correction = if with_matrix {
Some(array![[1.0, 0.0], [0.0, 1.0]])
} else {
None
};
SmoothingCorrectionOutcome::FirstOrder {
correction,
reason,
severity,
}
}
#[test]
fn cubature_branch_label_and_extraction() {
let outcome = SmoothingCorrectionOutcome::Cubature {
correction: array![[2.0, 0.0], [0.0, 2.0]],
rank: 2,
n_points: 4,
near_boundary: true,
grad_norm: 1.5,
max_rho_var: 0.7,
};
assert_eq!(outcome.branch_label(), "cubature");
let mat = outcome
.into_correction()
.expect("cubature always has a matrix");
assert_eq!(mat.dim(), (2, 2));
assert_eq!(mat[[0, 0]], 2.0);
}
#[test]
fn first_order_routine_branch_label_and_extraction() {
let outcome = make_first_order(
"n_rho == 0",
SmoothingCorrectionFallbackSeverity::Routine,
true,
);
assert_eq!(outcome.branch_label(), "first-order (routine)");
assert!(outcome.into_correction().is_some());
}
#[test]
fn first_order_numerical_branch_label_and_extraction() {
let outcome = make_first_order(
"rho Hessian inversion failed after ridge regularization",
SmoothingCorrectionFallbackSeverity::NumericalFailure,
true,
);
assert_eq!(outcome.branch_label(), "first-order (numerical failure)");
assert!(outcome.into_correction().is_some());
}
#[test]
fn first_order_without_matrix_returns_none() {
let outcome = make_first_order(
"no base covariance supplied",
SmoothingCorrectionFallbackSeverity::Routine,
false,
);
assert!(outcome.into_correction().is_none());
}
#[test]
fn severity_counter_is_monotonic() {
let before = SMOOTHING_CORRECTION_NUMERICAL_FAILURE_COUNT.load(Ordering::Relaxed);
SMOOTHING_CORRECTION_NUMERICAL_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
let after = SMOOTHING_CORRECTION_NUMERICAL_FAILURE_COUNT.load(Ordering::Relaxed);
assert!(
after > before,
"numerical-failure counter must be monotonic ({} -> {})",
before,
after
);
}
#[test]
fn cubature_counter_is_observable() {
let before = SMOOTHING_CORRECTION_CUBATURE_COUNT.load(Ordering::Relaxed);
SMOOTHING_CORRECTION_CUBATURE_COUNT.fetch_add(1, Ordering::Relaxed);
let after = SMOOTHING_CORRECTION_CUBATURE_COUNT.load(Ordering::Relaxed);
assert!(after > before);
}
#[test]
fn classification_reason_strings_are_nonempty_and_distinct() {
let reasons = [
"n_rho == 0: unified corrected covariance equals H^{-1}",
"n_rho exceeds AUTO_CUBATURE_MAX_RHO_DIM: cubature cost prohibitive",
"beta dimension exceeds AUTO_CUBATURE_MAX_BETA_DIM: cubature cost prohibitive",
"linearization sufficient: not near boundary and outer gradient is small",
"first-order V_rho rank-deficient: cubature would impute spurious variance",
"post-inversion rho posterior variance below trigger threshold",
"no base covariance supplied: nothing for cubature to upgrade",
"rho Hessian compute_lamlhessian_consistent failed",
"rho Hessian inversion failed after ridge regularization",
"eigendecomposition of inverse rho-Hessian failed",
"inverse rho-Hessian has no positive eigenvalues above numerical floor",
"positive-eigenvalue total mass non-finite or non-positive",
"variance-truncation produced rank 0 (unreachable guard)",
"empty sigma-point set (unreachable guard)",
"one or more sigma-point inner PIRLS fits failed",
"assembled total covariance contains non-finite entries",
];
for r in reasons.iter() {
assert!(!r.is_empty(), "classification reason must not be empty");
let routine = make_first_order(r, SmoothingCorrectionFallbackSeverity::Routine, true);
let numerical = make_first_order(
r,
SmoothingCorrectionFallbackSeverity::NumericalFailure,
true,
);
assert_eq!(routine.branch_label(), "first-order (routine)");
assert_eq!(numerical.branch_label(), "first-order (numerical failure)");
}
let mut sorted: Vec<&'static str> = reasons.to_vec();
sorted.sort();
sorted.dedup();
assert_eq!(
sorted.len(),
reasons.len(),
"classification reasons must be distinct so callers can disambiguate"
);
}
}