#![allow(non_snake_case)]
use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
use sklears_core::error::{Result as SklResult, SklearsError};
use std::f64::consts::PI;
use std::hash::Hash;
pub fn robust_cholesky(K: &Array2<f64>) -> SklResult<Array2<f64>> {
let n = K.nrows();
if n == 0 {
return Ok(Array2::zeros((0, 0)));
}
if n != K.ncols() {
return Err(SklearsError::InvalidInput(
"Matrix must be square for Cholesky decomposition".to_string(),
));
}
if let Ok(L) = cholesky_decomposition(K) {
return Ok(L);
}
let mut mean_diag = 0.0;
for i in 0..n {
mean_diag += K[[i, i]];
}
mean_diag /= n as f64;
let scale = if mean_diag.is_finite() && mean_diag > 0.0 {
mean_diag
} else {
1.0
};
let mut rel_jitter = 1e-10_f64;
let max_rel_jitter = 1e-2_f64;
while rel_jitter <= max_rel_jitter {
let jitter = rel_jitter * scale;
let mut K_jittered = K.clone();
for i in 0..n {
K_jittered[[i, i]] += jitter;
}
if let Ok(L) = cholesky_decomposition(&K_jittered) {
return Ok(L);
}
rel_jitter *= 10.0;
}
Err(SklearsError::NumericalError(
"Cholesky decomposition failed: matrix is not positive definite even \
after trace-proportional nugget regularization up to 1% of the mean diagonal"
.to_string(),
))
}
pub fn cholesky_decomposition(A: &Array2<f64>) -> SklResult<Array2<f64>> {
let n = A.nrows();
if n != A.ncols() {
return Err(SklearsError::InvalidInput(
"Matrix must be square for Cholesky decomposition".to_string(),
));
}
let mut L = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..=i {
if i == j {
let sum: f64 = (0..j).map(|k| L[[i, k]].powi(2)).sum();
let val = A[[i, i]] - sum;
if val <= 0.0 {
return Err(SklearsError::NumericalError(format!(
"Matrix is not positive definite at diagonal element {}",
i
)));
}
L[[i, j]] = val.sqrt();
} else {
let sum: f64 = (0..j).map(|k| L[[i, k]] * L[[j, k]]).sum();
if L[[j, j]] == 0.0 {
return Err(SklearsError::NumericalError(
"Division by zero in Cholesky decomposition".to_string(),
));
}
L[[i, j]] = (A[[i, j]] - sum) / L[[j, j]];
}
}
}
Ok(L)
}
pub fn triangular_solve(L: &Array2<f64>, b: &Array1<f64>) -> SklResult<Array1<f64>> {
let n = L.nrows();
if n != b.len() || n != L.ncols() {
return Err(SklearsError::InvalidInput(
"Dimension mismatch in triangular solve".to_string(),
));
}
let mut y = Array1::<f64>::zeros(n);
for i in 0..n {
let mut sum = 0.0;
for j in 0..i {
sum += L[[i, j]] * y[j];
}
if L[[i, i]].abs() < 1e-14 {
return Err(SklearsError::NumericalError(format!(
"Near-singular matrix in triangular solve at row {}",
i
)));
}
y[i] = (b[i] - sum) / L[[i, i]];
}
let mut x = Array1::<f64>::zeros(n);
for i in (0..n).rev() {
let mut sum = 0.0;
for j in (i + 1)..n {
sum += L[[j, i]] * x[j]; }
if L[[i, i]].abs() < 1e-14 {
return Err(SklearsError::NumericalError(format!(
"Near-singular matrix in backward substitution at row {}",
i
)));
}
x[i] = (y[i] - sum) / L[[i, i]];
}
Ok(x)
}
pub fn log_marginal_likelihood(L: &Array2<f64>, alpha: &Array1<f64>, y: &Array1<f64>) -> f64 {
let n = y.len() as f64;
let log_det_K = 2.0 * L.diag().mapv(|x| x.ln()).sum();
let quadratic_term = alpha.dot(y);
-0.5 * quadratic_term - 0.5 * log_det_K - 0.5 * n * (2.0 * PI).ln()
}
pub fn estimate_condition_number(K: &Array2<f64>) -> f64 {
let diag = K.diag();
let max_diag = diag.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let min_diag = diag.fold(f64::INFINITY, |a, &b| a.min(b));
if min_diag <= 0.0 {
f64::INFINITY
} else {
max_diag / min_diag
}
}
pub fn add_jitter(K: &mut Array2<f64>, jitter: f64) {
for i in 0..K.nrows() {
K[[i, i]] += jitter;
}
}
pub fn kmeans_inducing_points(
X: &ArrayView2<f64>,
n_inducing: usize,
random_state: Option<u64>,
) -> SklResult<Array2<f64>> {
let n_samples = X.nrows();
let n_features = X.ncols();
if n_inducing >= n_samples {
return Ok(X.to_owned());
}
let rng = match random_state {
Some(seed) => {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let mut hasher = DefaultHasher::new();
seed.hash(&mut hasher);
hasher.finish()
}
None => 42, };
let mut centroids = Array2::<f64>::zeros((n_inducing, n_features));
for i in 0..n_inducing {
let idx = (rng as usize + i * 1337) % n_samples;
centroids.row_mut(i).assign(&X.row(idx));
}
for _iter in 0..10 {
let mut assignments = vec![0; n_samples];
#[allow(clippy::needless_range_loop)]
for i in 0..n_samples {
let mut min_dist = f64::INFINITY;
for j in 0..n_inducing {
let dist = (&X.row(i) - ¢roids.row(j)).mapv(|x| x.powi(2)).sum();
if dist < min_dist {
min_dist = dist;
assignments[i] = j;
}
}
}
for j in 0..n_inducing {
let mut count = 0;
let mut sum = Array1::<f64>::zeros(n_features);
#[allow(clippy::needless_range_loop)]
for i in 0..n_samples {
if assignments[i] == j {
sum = sum + X.row(i);
count += 1;
}
}
if count > 0 {
centroids.row_mut(j).assign(&(sum / count as f64));
}
}
}
Ok(centroids)
}
pub fn uniform_inducing_points(
X: &ArrayView2<f64>,
n_inducing: usize,
random_state: Option<u64>,
) -> SklResult<Array2<f64>> {
let n_samples = X.nrows();
if n_inducing >= n_samples {
return Ok(X.to_owned());
}
let mut rng = random_state.unwrap_or(42);
let mut inducing_points = Array2::<f64>::zeros((n_inducing, X.ncols()));
let mut used_indices = std::collections::HashSet::new();
for i in 0..n_inducing {
let mut idx;
loop {
idx = (rng as usize + i * 1337 + i * i * 7919) % n_samples;
if !used_indices.contains(&idx) {
used_indices.insert(idx);
break;
}
rng = rng.wrapping_mul(1103515245).wrapping_add(12345); }
inducing_points.row_mut(i).assign(&X.row(idx));
}
Ok(inducing_points)
}
pub fn random_inducing_points(
X: &ArrayView2<f64>,
n_inducing: usize,
random_state: Option<u64>,
) -> SklResult<Array2<f64>> {
uniform_inducing_points(X, n_inducing, random_state)
}
pub fn triangular_solve_matrix(L: &Array2<f64>, B: &Array2<f64>) -> SklResult<Array2<f64>> {
let n = L.nrows();
let m = B.ncols();
if n != B.nrows() || n != L.ncols() {
return Err(SklearsError::InvalidInput(
"Dimension mismatch in triangular solve matrix".to_string(),
));
}
let mut X = Array2::<f64>::zeros((n, m));
for j in 0..m {
let b_col = B.column(j).to_owned();
let x_col = triangular_solve(L, &b_col)?;
X.column_mut(j).assign(&x_col);
}
Ok(X)
}
#[allow(non_snake_case)]
pub fn matrix_inverse(A: &Array2<f64>) -> SklResult<Array2<f64>> {
let n = A.nrows();
if n != A.ncols() {
return Err(SklearsError::InvalidInput(
"Matrix must be square for inversion".to_string(),
));
}
let L = robust_cholesky(A)?;
let I = Array2::<f64>::eye(n);
let Y = triangular_solve_matrix(&L, &I)?;
let L_T = L.t().to_owned();
let A_inv = triangular_solve_matrix(&L_T, &Y)?;
Ok(A_inv.t().to_owned())
}
pub fn cholesky(A: &Array2<f64>) -> SklResult<Array2<f64>> {
cholesky_decomposition(A)
}
#[allow(non_snake_case)]
pub fn log_determinant(A: &Array2<f64>) -> SklResult<f64> {
let L = robust_cholesky(A)?;
let log_det = 2.0 * L.diag().mapv(|x| x.ln()).sum();
Ok(log_det)
}
pub fn kl_divergence_gaussian(
mu1: &Array1<f64>,
sigma1: &Array2<f64>,
mu2: &Array1<f64>,
sigma2: &Array2<f64>,
) -> SklResult<f64> {
let k = mu1.len() as f64;
let sigma2_inv = matrix_inverse(sigma2)?;
let trace_term = (&sigma2_inv * sigma1).sum();
let mu_diff = mu2 - mu1;
let quad_term = mu_diff.dot(&sigma2_inv.dot(&mu_diff));
let log_det_sigma2 = log_determinant(sigma2)?;
let log_det_sigma1 = log_determinant(sigma1)?;
let kl_div = 0.5 * (trace_term + quad_term - k + log_det_sigma2 - log_det_sigma1);
Ok(kl_div)
}
pub fn ensure_positive_definite(A: &Array2<f64>) -> SklResult<Array2<f64>> {
let mut A_reg = A.clone();
let n = A.nrows();
match cholesky_decomposition(&A_reg) {
Ok(_) => return Ok(A_reg),
Err(_) => {
let min_eigenvalue = estimate_min_eigenvalue(&A_reg);
let regularization = if min_eigenvalue <= 0.0 {
1e-6 - min_eigenvalue
} else {
1e-6
};
for i in 0..n {
A_reg[[i, i]] += regularization;
}
}
}
Ok(A_reg)
}
fn estimate_min_eigenvalue(A: &Array2<f64>) -> f64 {
let n = A.nrows();
if n == 0 {
return 0.0;
}
A.diag().iter().cloned().fold(f64::INFINITY, f64::min)
}