use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
use crate::error::{ClusteringError, Result};
pub struct SparseSubspaceClustering {
pub lambda1: f64,
pub lambda2: f64,
pub rho: f64,
pub max_iter: usize,
pub tol: f64,
pub gs_iter: usize,
}
impl Default for SparseSubspaceClustering {
fn default() -> Self {
Self {
lambda1: 0.1,
lambda2: 0.01,
rho: 1.0,
max_iter: 200,
tol: 1e-6,
gs_iter: 20,
}
}
}
impl SparseSubspaceClustering {
pub fn fit(
&self,
x: ArrayView2<f64>,
lambda: Option<f64>,
max_iter: Option<usize>,
n_clusters: usize,
) -> Result<Array1<usize>> {
let (n, _) = (x.shape()[0], x.shape()[1]);
let lambda1 = lambda.unwrap_or(self.lambda1);
let max_it = max_iter.unwrap_or(self.max_iter);
if n == 0 {
return Err(ClusteringError::InvalidInput("Empty data matrix".into()));
}
if n_clusters == 0 || n_clusters > n {
return Err(ClusteringError::InvalidInput(format!(
"n_clusters ({n_clusters}) must be in [1, n_samples ({n})]"
)));
}
if lambda1 < 0.0 {
return Err(ClusteringError::InvalidInput(
"lambda must be non-negative".into(),
));
}
let gram = compute_gram(x);
let c_mat =
ssc_admm_elasticnet(&gram, lambda1, self.lambda2, self.rho, max_it, self.tol, self.gs_iter)?;
let affinity = symmetrise_abs(&c_mat);
let labels = spectral_on_affinity(&affinity, n_clusters)?;
Ok(labels)
}
}
pub struct LowRankSubspaceClustering {
pub lambda: f64,
pub rho: f64,
pub max_iter: usize,
pub tol: f64,
}
impl Default for LowRankSubspaceClustering {
fn default() -> Self {
Self {
lambda: 1.0,
rho: 0.5,
max_iter: 100,
tol: 1e-5,
}
}
}
impl LowRankSubspaceClustering {
pub fn fit(
&self,
x: ArrayView2<f64>,
lambda: Option<f64>,
max_iter: Option<usize>,
n_clusters: usize,
) -> Result<Array1<usize>> {
let n = x.shape()[0];
let lam = lambda.unwrap_or(self.lambda);
let max_it = max_iter.unwrap_or(self.max_iter);
if n == 0 {
return Err(ClusteringError::InvalidInput("Empty data matrix".into()));
}
if n_clusters == 0 || n_clusters > n {
return Err(ClusteringError::InvalidInput(format!(
"n_clusters ({n_clusters}) must be in [1, n_samples ({n})]"
)));
}
if lam < 0.0 {
return Err(ClusteringError::InvalidInput(
"lambda must be non-negative".into(),
));
}
let gram = compute_gram(x);
let c_mat = lrsc_admm_svt(&gram, lam, self.rho, max_it, self.tol)?;
let affinity = symmetrise_abs(&c_mat);
let labels = spectral_on_affinity(&affinity, n_clusters)?;
Ok(labels)
}
}
pub struct ThresholdingSubspace {
pub proj_ratio: f64,
pub k_neighbors: usize,
pub threshold: f64,
pub seed: u64,
}
impl Default for ThresholdingSubspace {
fn default() -> Self {
Self {
proj_ratio: 0.5,
k_neighbors: 5,
threshold: 0.5,
seed: 42,
}
}
}
impl ThresholdingSubspace {
pub fn fit(
&self,
x: ArrayView2<f64>,
threshold: Option<f64>,
n_clusters: usize,
) -> Result<Array1<usize>> {
let (n, d) = (x.shape()[0], x.shape()[1]);
let thresh = threshold.unwrap_or(self.threshold);
if n == 0 {
return Err(ClusteringError::InvalidInput("Empty data matrix".into()));
}
if n_clusters == 0 || n_clusters > n {
return Err(ClusteringError::InvalidInput(format!(
"n_clusters ({n_clusters}) must be in [1, n_samples ({n})]"
)));
}
if d == 0 {
return Err(ClusteringError::InvalidInput(
"Data must have at least one feature".into(),
));
}
let x_norm = l2_normalise_rows_copy(x);
let proj_dim = ((d as f64 * self.proj_ratio).ceil() as usize)
.max(1)
.min(d);
let mut rng = self.seed;
let proj_mat = random_gaussian_matrix(d, proj_dim, &mut rng);
let projected = mat_mul(&x_norm, &proj_mat);
let k = self.k_neighbors.min(n - 1).max(1);
let mut affinity = Array2::<f64>::zeros((n, n));
for i in 0..n {
let mut dists: Vec<(usize, f64)> = (0..n)
.filter(|&j| j != i)
.map(|j| {
let mut s = 0.0;
for l in 0..proj_dim {
let diff = projected[[i, l]] - projected[[j, l]];
s += diff * diff;
}
(j, s)
})
.collect();
dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let mut kept = 0usize;
for (j, _proj_dist) in dists.iter().take(k * 4) {
let cos: f64 = (0..d).map(|l| x_norm[[i, l]] * x_norm[[*j, l]]).sum();
let cos_abs = cos.abs();
if cos_abs >= thresh {
affinity[[i, *j]] = cos_abs;
affinity[[*j, i]] = cos_abs;
kept += 1;
if kept >= k {
break;
}
}
}
if kept == 0 {
if let Some(&(j, _)) = dists.first() {
let cos: f64 = (0..d).map(|l| x_norm[[i, l]] * x_norm[[j, l]]).sum();
affinity[[i, j]] = cos.abs().max(1e-10);
affinity[[j, i]] = affinity[[i, j]];
}
}
}
let labels = spectral_on_affinity(&affinity, n_clusters)?;
Ok(labels)
}
}
fn compute_gram(x: ArrayView2<f64>) -> Array2<f64> {
let (n, d) = (x.shape()[0], x.shape()[1]);
let mut g = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..=i {
let mut s = 0.0;
for l in 0..d {
s += x[[i, l]] * x[[j, l]];
}
g[[i, j]] = s;
g[[j, i]] = s;
}
}
g
}
fn ssc_admm_elasticnet(
gram: &Array2<f64>,
lambda1: f64,
lambda2: f64,
rho: f64,
max_iter: usize,
tol: f64,
gs_iter: usize,
) -> Result<Array2<f64>> {
let n = gram.shape()[0];
let mut c_mat = Array2::<f64>::zeros((n, n));
let mut z_mat = Array2::<f64>::zeros((n, n));
let mut u_mat = Array2::<f64>::zeros((n, n));
let mut a_mat = gram.clone();
let diag_add = rho + lambda2;
for i in 0..n {
a_mat[[i, i]] += diag_add;
}
let thresh = lambda1 / rho;
for _iter in 0..max_iter {
for j in 0..n {
let mut rhs = Array1::<f64>::zeros(n);
for i in 0..n {
rhs[i] = gram[[i, j]] + rho * (z_mat[[i, j]] - u_mat[[i, j]]);
}
let mut x_col = Array1::<f64>::zeros(n);
for _gs in 0..gs_iter {
for i in 0..n {
let mut sum = rhs[i];
for k in 0..n {
if k != i {
sum -= a_mat[[i, k]] * x_col[k];
}
}
let diag = a_mat[[i, i]];
if diag.abs() > f64::EPSILON {
x_col[i] = sum / diag;
}
}
}
x_col[j] = 0.0;
for i in 0..n {
c_mat[[i, j]] = x_col[i];
}
}
let mut primal_res = 0.0f64;
for i in 0..n {
for j in 0..n {
let v = c_mat[[i, j]] + u_mat[[i, j]];
let new_z = soft_threshold(v, thresh);
primal_res += (c_mat[[i, j]] - new_z).powi(2);
z_mat[[i, j]] = new_z;
}
}
for i in 0..n {
for j in 0..n {
u_mat[[i, j]] += c_mat[[i, j]] - z_mat[[i, j]];
}
}
if primal_res.sqrt() < tol {
break;
}
}
Ok(c_mat)
}
fn lrsc_admm_svt(
gram: &Array2<f64>,
lambda: f64,
rho: f64,
max_iter: usize,
tol: f64,
) -> Result<Array2<f64>> {
let n = gram.shape()[0];
let mut c_mat = Array2::<f64>::zeros((n, n));
let mut z_mat = Array2::<f64>::zeros((n, n));
let mut u_mat = Array2::<f64>::zeros((n, n));
let mut a_mat = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
a_mat[[i, j]] = lambda * gram[[i, j]];
}
a_mat[[i, i]] += rho;
}
let svt_thresh = 1.0 / rho;
for _iter in 0..max_iter {
for j in 0..n {
let mut rhs = Array1::<f64>::zeros(n);
for i in 0..n {
rhs[i] = lambda * gram[[i, j]] + rho * (z_mat[[i, j]] - u_mat[[i, j]]);
}
let mut x_col = cg_solve(&a_mat, &rhs, 30, tol * 0.1);
x_col[j] = 0.0;
for i in 0..n {
c_mat[[i, j]] = x_col[i];
}
}
let cu = {
let mut m = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
m[[i, j]] = c_mat[[i, j]] + u_mat[[i, j]];
}
}
m
};
let (u_svd, s_svd, vt_svd) = compact_svd(cu.view(), n, 42)?;
let mut primal_res = 0.0f64;
z_mat = Array2::<f64>::zeros((n, n));
for k in 0..s_svd.len() {
let sv = (s_svd[k] - svt_thresh).max(0.0);
if sv > 0.0 {
for i in 0..n {
for j in 0..n {
z_mat[[i, j]] += sv * u_svd[[i, k]] * vt_svd[[k, j]];
}
}
}
}
for i in 0..n {
for j in 0..n {
let r = c_mat[[i, j]] - z_mat[[i, j]];
u_mat[[i, j]] += r;
primal_res += r * r;
}
}
if primal_res.sqrt() < tol {
break;
}
}
Ok(c_mat)
}
fn cg_solve(a: &Array2<f64>, b: &Array1<f64>, max_iter: usize, tol: f64) -> Array1<f64> {
let n = b.len();
let mut x = Array1::<f64>::zeros(n);
let mut r = b.clone();
let mut p = r.clone();
let mut r_sq: f64 = r.iter().map(|v| v * v).sum();
for _ in 0..max_iter {
if r_sq < tol * tol {
break;
}
let mut q = Array1::<f64>::zeros(n);
for i in 0..n {
for j in 0..n {
q[i] += a[[i, j]] * p[j];
}
}
let pq: f64 = p.iter().zip(q.iter()).map(|(a, b)| a * b).sum();
if pq.abs() < f64::EPSILON {
break;
}
let alpha = r_sq / pq;
for i in 0..n {
x[i] += alpha * p[i];
r[i] -= alpha * q[i];
}
let r_sq_new: f64 = r.iter().map(|v| v * v).sum();
let beta = r_sq_new / r_sq.max(f64::EPSILON);
for i in 0..n {
p[i] = r[i] + beta * p[i];
}
r_sq = r_sq_new;
}
x
}
fn symmetrise_abs(c: &Array2<f64>) -> Array2<f64> {
let n = c.shape()[0];
let mut w = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
w[[i, j]] = c[[i, j]].abs() + c[[j, i]].abs();
}
}
w
}
fn spectral_on_affinity(affinity: &Array2<f64>, k: usize) -> Result<Array1<usize>> {
let n = affinity.shape()[0];
if n <= k {
let labels: Vec<usize> = (0..n).map(|i| i % k).collect();
return Ok(Array1::from_vec(labels));
}
let mut degree = vec![0.0f64; n];
for i in 0..n {
for j in 0..n {
degree[i] += affinity[[i, j]];
}
}
let mut m = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let di = if degree[i] > f64::EPSILON {
1.0 / degree[i].sqrt()
} else {
0.0
};
let dj = if degree[j] > f64::EPSILON {
1.0 / degree[j].sqrt()
} else {
0.0
};
m[[i, j]] = di * affinity[[i, j]] * dj;
}
}
let mut embedding = Array2::<f64>::zeros((n, k));
let mut deflated = m.clone();
let mut seed = 0xDEADBEEF_u64;
for kk in 0..k {
let v = power_iteration_vec(&deflated, 150, &mut seed);
for i in 0..n {
embedding[[i, kk]] = v[i];
}
let mut eigenval = 0.0f64;
for i in 0..n {
let mut av_i = 0.0f64;
for j in 0..n {
av_i += deflated[[i, j]] * v[j];
}
eigenval += v[i] * av_i;
}
for i in 0..n {
for j in 0..n {
deflated[[i, j]] -= eigenval * v[i] * v[j];
}
}
}
for i in 0..n {
let norm: f64 = (0..k)
.map(|kk| embedding[[i, kk]] * embedding[[i, kk]])
.sum::<f64>()
.sqrt();
if norm > 1e-12 {
for kk in 0..k {
embedding[[i, kk]] /= norm;
}
}
}
let mut rng = 0xCAFEBABE_u64;
let i_labels = kmeans_usize(embedding.view(), k, 100, &mut rng);
Ok(i_labels)
}
fn power_iteration_vec(mat: &Array2<f64>, max_iter: usize, rng: &mut u64) -> Vec<f64> {
let n = mat.shape()[0];
let mut v: Vec<f64> = (0..n).map(|_| lcg_f64(rng) - 0.5).collect();
normalise_vec_f64(&mut v);
for _ in 0..max_iter {
let mut av = vec![0.0f64; n];
for i in 0..n {
for j in 0..n {
av[i] += mat[[i, j]] * v[j];
}
}
normalise_vec_f64(&mut av);
v = av;
}
v
}
fn normalise_vec_f64(v: &mut Vec<f64>) {
let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm > 1e-12 {
for x in v.iter_mut() {
*x /= norm;
}
}
}
fn kmeans_usize(
features: ArrayView2<f64>,
k: usize,
max_iter: usize,
rng: &mut u64,
) -> Array1<usize> {
let (n, d) = (features.shape()[0], features.shape()[1]);
if n == 0 || k == 0 {
return Array1::zeros(n);
}
let k = k.min(n);
let step = (n as f64 / k as f64).max(1.0);
let mut centroids: Vec<Vec<f64>> = (0..k)
.map(|ci| {
let idx = ((ci as f64 * step) as usize).min(n - 1);
features.row(idx).to_vec()
})
.collect();
let mut labels = vec![0usize; n];
for _iter in 0..max_iter {
let mut changed = false;
for i in 0..n {
let best = (0..k)
.map(|j| {
let dist: f64 = (0..d)
.map(|l| (features[[i, l]] - centroids[j][l]).powi(2))
.sum();
(j, dist)
})
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(j, _)| j)
.unwrap_or(0);
if labels[i] != best {
labels[i] = best;
changed = true;
}
}
if !changed {
break;
}
let mut sums: Vec<Vec<f64>> = vec![vec![0.0; d]; k];
let mut counts = vec![0usize; k];
for i in 0..n {
let c = labels[i];
counts[c] += 1;
for l in 0..d {
sums[c][l] += features[[i, l]];
}
}
for j in 0..k {
if counts[j] == 0 {
let ri = lcg_usize(rng, n);
centroids[j] = features.row(ri).to_vec();
} else {
for l in 0..d {
centroids[j][l] = sums[j][l] / counts[j] as f64;
}
}
}
}
Array1::from_vec(labels)
}
fn soft_threshold(x: f64, t: f64) -> f64 {
let ax = x.abs();
if ax <= t {
0.0
} else if x > 0.0 {
ax - t
} else {
t - ax
}
}
fn l2_normalise_rows_copy(x: ArrayView2<f64>) -> Array2<f64> {
let (n, d) = (x.shape()[0], x.shape()[1]);
let mut out = Array2::<f64>::zeros((n, d));
for r in 0..n {
let norm: f64 = (0..d).map(|c| x[[r, c]] * x[[r, c]]).sum::<f64>().sqrt();
if norm > 1e-12 {
for c in 0..d {
out[[r, c]] = x[[r, c]] / norm;
}
}
}
out
}
fn mat_mul(a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
let (m, k) = (a.shape()[0], a.shape()[1]);
let n = b.shape()[1];
let mut c = Array2::<f64>::zeros((m, n));
for i in 0..m {
for l in 0..k {
let a_il = a[[i, l]];
if a_il == 0.0 {
continue;
}
for j in 0..n {
c[[i, j]] += a_il * b[[l, j]];
}
}
}
c
}
fn random_gaussian_matrix(rows: usize, cols: usize, rng: &mut u64) -> Array2<f64> {
let mut m = Array2::<f64>::zeros((rows, cols));
let scale = 1.0 / (rows as f64).sqrt();
for v in m.iter_mut() {
let u1 = lcg_f64(rng).max(1e-15);
let u2 = lcg_f64(rng);
*v = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos() * scale;
}
m
}
fn lcg_f64(state: &mut u64) -> f64 {
*state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(*state >> 11) as f64 / (1u64 << 53) as f64
}
fn lcg_usize(state: &mut u64, n: usize) -> usize {
(lcg_f64(state) * n as f64) as usize % n
}
fn compact_svd(
x: ArrayView2<f64>,
k: usize,
seed: u64,
) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>)> {
let (m, n) = (x.shape()[0], x.shape()[1]);
let k = k.min(m).min(n).max(1);
let mut rng = seed;
let mut omega = Array2::<f64>::zeros((n, k));
for v in omega.iter_mut() {
let u1 = lcg_f64(&mut rng).max(1e-15);
let u2 = lcg_f64(&mut rng);
*v = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
}
let y = mat_mul_arr(x, omega.view());
let q = gram_schmidt_arr(y.view())?;
let b = mat_mul_arr_t(q.view(), x);
let bbt = mat_mul_arr(b.view(), {
let mut bt = Array2::<f64>::zeros((n, k));
for i in 0..k {
for j in 0..n {
bt[[j, i]] = b[[i, j]];
}
}
bt
}.view());
let (ub, sigma) = power_iter_eig_local(bbt.view(), k, seed.wrapping_add(1))?;
let s: Array1<f64> = sigma.mapv(|v| v.max(0.0).sqrt());
let u = mat_mul_arr(q.view(), ub.view());
let mut vt = Array2::<f64>::zeros((k, n));
for i in 0..k {
let si = s[i];
if si < 1e-12 {
continue;
}
for j in 0..n {
let mut val = 0.0;
for l in 0..k {
val += ub[[l, i]] * b[[l, j]];
}
vt[[i, j]] = val / si;
}
}
Ok((u, s, vt))
}
fn mat_mul_arr(a: ArrayView2<f64>, b: ArrayView2<f64>) -> Array2<f64> {
let (m, k) = (a.shape()[0], a.shape()[1]);
let n = b.shape()[1];
let mut c = Array2::<f64>::zeros((m, n));
for i in 0..m {
for l in 0..k {
let a_il = a[[i, l]];
if a_il == 0.0 {
continue;
}
for j in 0..n {
c[[i, j]] += a_il * b[[l, j]];
}
}
}
c
}
fn mat_mul_arr_t(a: ArrayView2<f64>, b: ArrayView2<f64>) -> Array2<f64> {
let (m, k) = (a.shape()[0], a.shape()[1]);
let n = b.shape()[1];
let mut c = Array2::<f64>::zeros((k, n));
for l in 0..k {
for i in 0..m {
let a_il = a[[i, l]];
if a_il == 0.0 {
continue;
}
for j in 0..n {
c[[l, j]] += a_il * b[[i, j]];
}
}
}
c
}
fn gram_schmidt_arr(a: ArrayView2<f64>) -> Result<Array2<f64>> {
let (m, n) = (a.shape()[0], a.shape()[1]);
let mut q = Array2::<f64>::zeros((m, n));
for j in 0..n {
let mut v: Vec<f64> = (0..m).map(|i| a[[i, j]]).collect();
for i in 0..j {
let dot: f64 = (0..m).map(|r| v[r] * q[[r, i]]).sum();
for r in 0..m {
v[r] -= dot * q[[r, i]];
}
}
let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm < 1e-12 {
if j < m {
q[[j, j]] = 1.0;
}
} else {
for r in 0..m {
q[[r, j]] = v[r] / norm;
}
}
}
Ok(q)
}
fn power_iter_eig_local(
a: ArrayView2<f64>,
k: usize,
seed: u64,
) -> Result<(Array2<f64>, Array1<f64>)> {
let n = a.shape()[0];
let k = k.min(n);
let mut rng = seed;
let mut eigvecs = Array2::<f64>::zeros((n, k));
let mut eigvals = Array1::<f64>::zeros(k);
let mut deflated = a.to_owned();
for col in 0..k {
let mut v: Vec<f64> = (0..n).map(|_| lcg_f64(&mut rng) - 0.5).collect();
normalise_vec_f64(&mut v);
for _ in 0..200 {
let mut av = vec![0.0f64; n];
for i in 0..n {
for j in 0..n {
av[i] += deflated[[i, j]] * v[j];
}
}
for prev in 0..col {
let dot: f64 = (0..n).map(|i| av[i] * eigvecs[[i, prev]]).sum();
for i in 0..n {
av[i] -= dot * eigvecs[[i, prev]];
}
}
normalise_vec_f64(&mut av);
v = av;
}
let mut av = vec![0.0f64; n];
for i in 0..n {
for j in 0..n {
av[i] += deflated[[i, j]] * v[j];
}
}
let eigenvalue: f64 = v.iter().zip(av.iter()).map(|(a, b)| a * b).sum();
eigvals[col] = eigenvalue;
for i in 0..n {
eigvecs[[i, col]] = v[i];
}
for i in 0..n {
for j in 0..n {
deflated[[i, j]] -= eigenvalue * v[i] * v[j];
}
}
}
Ok((eigvecs, eigvals))
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn three_subspace_data() -> Array2<f64> {
let n_per = 10;
let n = n_per * 3;
let d = 6;
let mut data = Array2::<f64>::zeros((n, d));
let mut rng = 0xABCDEF_u64;
fn lcg(s: &mut u64) -> f64 {
*s = s.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
(*s >> 11) as f64 / (1u64 << 53) as f64 - 0.5
}
for i in 0..n_per {
let a = lcg(&mut rng);
let b = lcg(&mut rng);
data[[i, 0]] = a;
data[[i, 1]] = b;
}
for i in 0..n_per {
let a = lcg(&mut rng);
let b = lcg(&mut rng);
data[[n_per + i, 2]] = a;
data[[n_per + i, 3]] = b;
}
for i in 0..n_per {
let a = lcg(&mut rng);
let b = lcg(&mut rng);
data[[2 * n_per + i, 4]] = a;
data[[2 * n_per + i, 5]] = b;
}
data
}
#[test]
fn test_ssc_output_shape() {
let x = three_subspace_data();
let ssc = SparseSubspaceClustering::default();
let labels = ssc.fit(x.view(), None, None, 3).expect("SSC failed");
assert_eq!(labels.len(), 30);
}
#[test]
fn test_ssc_labels_in_range() {
let x = three_subspace_data();
let ssc = SparseSubspaceClustering::default();
let labels = ssc.fit(x.view(), None, None, 3).expect("SSC failed");
for &l in labels.iter() {
assert!(l < 3, "label {l} out of range");
}
}
#[test]
fn test_ssc_lambda_override() {
let x = three_subspace_data();
let ssc = SparseSubspaceClustering {
lambda1: 0.5,
..Default::default()
};
let labels = ssc.fit(x.view(), Some(0.2), Some(50), 3).expect("SSC lambda override");
assert_eq!(labels.len(), 30);
}
#[test]
fn test_ssc_invalid_input() {
let x = three_subspace_data();
let ssc = SparseSubspaceClustering::default();
assert!(ssc.fit(x.view(), None, None, 100).is_err());
assert!(ssc.fit(x.view(), None, None, 0).is_err());
let empty = Array2::<f64>::zeros((0, 6));
assert!(ssc.fit(empty.view(), None, None, 3).is_err());
assert!(ssc.fit(x.view(), Some(-0.1), None, 3).is_err());
}
#[test]
fn test_ssc_two_clusters() {
let mut data = Array2::<f64>::zeros((10, 4));
for i in 0..5 {
data[[i, 0]] = (i as f64 + 1.0) * 0.3;
data[[i, 1]] = (i as f64 + 1.0) * 0.2;
}
for i in 5..10 {
data[[i, 2]] = (i as f64 - 4.0) * 0.3;
data[[i, 3]] = (i as f64 - 4.0) * 0.2;
}
let ssc = SparseSubspaceClustering::default();
let labels = ssc.fit(data.view(), None, None, 2).expect("SSC 2 clusters");
assert_eq!(labels.len(), 10);
}
#[test]
fn test_lrsc_output_shape() {
let x = three_subspace_data();
let lrsc = LowRankSubspaceClustering::default();
let labels = lrsc.fit(x.view(), None, None, 3).expect("LRSC failed");
assert_eq!(labels.len(), 30);
}
#[test]
fn test_lrsc_labels_in_range() {
let x = three_subspace_data();
let lrsc = LowRankSubspaceClustering::default();
let labels = lrsc.fit(x.view(), None, None, 3).expect("LRSC failed");
for &l in labels.iter() {
assert!(l < 3, "LRSC label {l} out of range");
}
}
#[test]
fn test_lrsc_lambda_override() {
let x = three_subspace_data();
let lrsc = LowRankSubspaceClustering::default();
let labels = lrsc.fit(x.view(), Some(2.0), Some(30), 3).expect("LRSC lambda override");
assert_eq!(labels.len(), 30);
}
#[test]
fn test_lrsc_invalid_input() {
let x = three_subspace_data();
let lrsc = LowRankSubspaceClustering::default();
assert!(lrsc.fit(x.view(), None, None, 0).is_err());
assert!(lrsc.fit(x.view(), None, None, 100).is_err());
assert!(lrsc.fit(x.view(), Some(-0.5), None, 3).is_err());
let empty = Array2::<f64>::zeros((0, 4));
assert!(lrsc.fit(empty.view(), None, None, 3).is_err());
}
#[test]
fn test_thresholding_output_shape() {
let x = three_subspace_data();
let ts = ThresholdingSubspace::default();
let labels = ts.fit(x.view(), None, 3).expect("Thresholding failed");
assert_eq!(labels.len(), 30);
}
#[test]
fn test_thresholding_labels_in_range() {
let x = three_subspace_data();
let ts = ThresholdingSubspace::default();
let labels = ts.fit(x.view(), None, 3).expect("Thresholding failed");
for &l in labels.iter() {
assert!(l < 3, "Thresholding label {l} out of range");
}
}
#[test]
fn test_thresholding_threshold_override() {
let x = three_subspace_data();
let ts = ThresholdingSubspace {
threshold: 0.3,
k_neighbors: 3,
..Default::default()
};
let labels = ts.fit(x.view(), Some(0.4), 3).expect("Thresholding override");
assert_eq!(labels.len(), 30);
}
#[test]
fn test_thresholding_invalid_input() {
let x = three_subspace_data();
let ts = ThresholdingSubspace::default();
assert!(ts.fit(x.view(), None, 0).is_err());
assert!(ts.fit(x.view(), None, 100).is_err());
let empty = Array2::<f64>::zeros((0, 4));
assert!(ts.fit(empty.view(), None, 3).is_err());
let no_features = Array2::<f64>::zeros((5, 0));
assert!(ts.fit(no_features.view(), None, 3).is_err());
}
#[test]
fn test_thresholding_two_subspaces() {
let mut data = Array2::<f64>::zeros((12, 4));
for i in 0..6 {
data[[i, 0]] = (i as f64 + 1.0) * 0.4;
data[[i, 1]] = (i as f64 + 1.0) * 0.3;
}
for i in 6..12 {
data[[i, 2]] = (i as f64 - 5.0) * 0.4;
data[[i, 3]] = (i as f64 - 5.0) * 0.3;
}
let ts = ThresholdingSubspace {
threshold: 0.2,
k_neighbors: 4,
..Default::default()
};
let labels = ts.fit(data.view(), None, 2).expect("Thresholding 2 subspaces");
assert_eq!(labels.len(), 12);
}
#[test]
fn test_soft_threshold() {
assert!((soft_threshold(2.0, 1.0) - 1.0).abs() < 1e-10);
assert!((soft_threshold(-2.0, 1.0) - (-1.0)).abs() < 1e-10);
assert!(soft_threshold(0.5, 1.0).abs() < 1e-10);
assert!(soft_threshold(-0.5, 1.0).abs() < 1e-10);
}
#[test]
fn test_compute_gram_positive_semidefinite() {
let x = three_subspace_data();
let g = compute_gram(x.view());
let n = g.shape()[0];
for i in 0..n {
assert!(g[[i, i]] >= 0.0, "g[{i},{i}] < 0: {}", g[[i, i]]);
}
for i in 0..n {
for j in 0..n {
let diff = (g[[i, j]] - g[[j, i]]).abs();
assert!(diff < 1e-10, "Gram not symmetric at ({i},{j})");
}
}
}
#[test]
fn test_symmetrise_abs_symmetry() {
let c = Array2::from_shape_vec(
(3, 3),
vec![0.0, 0.3, -0.5, 0.1, 0.0, 0.4, -0.2, 0.6, 0.0],
)
.expect("shape");
let w = symmetrise_abs(&c);
for i in 0..3 {
for j in 0..3 {
let diff = (w[[i, j]] - w[[j, i]]).abs();
assert!(diff < 1e-10, "symmetrise_abs not symmetric at ({i},{j})");
}
}
}
#[test]
fn test_cg_solve_identity() {
let n = 5;
let mut a = Array2::<f64>::zeros((n, n));
for i in 0..n {
a[[i, i]] = 1.0;
}
let b = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let x = cg_solve(&a, &b, 20, 1e-10);
for i in 0..n {
assert!((x[i] - b[i]).abs() < 1e-8, "CG solve error at {i}");
}
}
}