use ndarray::{Array1, Array2, ArrayView2};
use solow_core::{Error, Result};
use solow_manifold::isomap::jacobi_symmetric;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum IcaFun {
LogCosh,
Exp,
}
impl IcaFun {
fn g_and_gprime(&self, u: f64) -> (f64, f64) {
match self {
IcaFun::LogCosh => {
let t = u.tanh();
(t, 1.0 - t * t)
}
IcaFun::Exp => {
let e = (-0.5 * u * u).exp();
(u * e, (1.0 - u * u) * e)
}
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct FastIca {
pub components: Array2<f64>,
pub whitening: Array2<f64>,
pub mean: Array1<f64>,
pub n_components: usize,
pub n_iter: usize,
}
impl FastIca {
pub fn fit(x: ArrayView2<'_, f64>, n_components: usize, seed: u64) -> Result<Self> {
Self::fit_with(x, n_components, IcaFun::LogCosh, 200, 1e-4, seed)
}
pub fn fit_with(
x: ArrayView2<'_, f64>,
n_components: usize,
fun: IcaFun,
max_iter: usize,
tol: f64,
seed: u64,
) -> Result<Self> {
if x.nrows() < 2 || x.ncols() == 0 {
return Err(Error::Value(
"FastIca::fit_with: need ≥ 2 samples and ≥ 1 feature".into(),
));
}
if n_components == 0 || n_components > x.ncols() {
return Err(Error::Value(format!(
"FastIca::fit_with: n_components in [1, d] (got {n_components})"
)));
}
let (n, d) = (x.nrows(), x.ncols());
let mut mean = Array1::<f64>::zeros(d);
for j in 0..d {
mean[j] = x.column(j).iter().sum::<f64>() / n as f64;
}
let mut xc = Array2::<f64>::zeros((n, d));
for i in 0..n {
for j in 0..d {
xc[[i, j]] = x[[i, j]] - mean[j];
}
}
let mut cov = Array2::<f64>::zeros((d, d));
for i in 0..d {
for j in 0..d {
let mut s = 0.0_f64;
for k in 0..n {
s += xc[[k, i]] * xc[[k, j]];
}
cov[[i, j]] = s / n as f64;
}
}
let (eigvals, eigvecs) = jacobi_symmetric(&cov, 300, 1e-12);
let mut order: Vec<usize> = (0..d).collect();
order.sort_by(|&a, &b| eigvals[b].partial_cmp(&eigvals[a]).unwrap());
let mut k = Array2::<f64>::zeros((n_components, d));
for c in 0..n_components {
let idx = order[c];
let s = eigvals[idx].max(1e-12).sqrt();
for j in 0..d {
k[[c, j]] = eigvecs[[j, idx]] / s;
}
}
let mut x1 = Array2::<f64>::zeros((n, n_components));
for i in 0..n {
for c in 0..n_components {
let mut s = 0.0_f64;
for j in 0..d {
s += xc[[i, j]] * k[[c, j]];
}
x1[[i, c]] = s;
}
}
let mut state = seed.wrapping_add(0xABCD_EF01_2345_6789);
let mut w = Array2::<f64>::zeros((n_components, n_components));
for i in 0..n_components {
for j in 0..n_components {
w[[i, j]] = uniform_symmetric(&mut state, 1.0);
}
}
symmetric_decorrelate(&mut w);
let mut n_iter_used = 0usize;
for it in 0..max_iter {
n_iter_used = it + 1;
let mut new_w = Array2::<f64>::zeros(w.dim());
for c in 0..n_components {
let (mut mean_g, mut mean_gp) = (0.0_f64, 0.0_f64);
let mut acc = Array1::<f64>::zeros(n_components);
for i in 0..n {
let mut u = 0.0_f64;
for kk in 0..n_components {
u += w[[c, kk]] * x1[[i, kk]];
}
let (g, gp) = fun.g_and_gprime(u);
mean_g += g;
mean_gp += gp;
for kk in 0..n_components {
acc[kk] += x1[[i, kk]] * g;
}
}
mean_g /= n as f64;
mean_gp /= n as f64;
for kk in 0..n_components {
new_w[[c, kk]] = acc[kk] / n as f64 - mean_gp * w[[c, kk]];
}
let _ = mean_g;
}
symmetric_decorrelate(&mut new_w);
let mut delta = 0.0_f64;
for c in 0..n_components {
let mut s = 0.0_f64;
for kk in 0..n_components {
s += new_w[[c, kk]] * w[[c, kk]];
}
delta = delta.max((s.abs() - 1.0).abs());
}
w = new_w;
if delta < tol {
break;
}
}
Ok(Self {
components: w,
whitening: k,
mean,
n_components,
n_iter: n_iter_used,
})
}
pub fn transform(&self, x: ArrayView2<'_, f64>) -> Array2<f64> {
let (n, d) = (x.nrows(), x.ncols());
let mut out = Array2::<f64>::zeros((n, self.n_components));
for i in 0..n {
let mut centered = vec![0.0_f64; d];
for j in 0..d {
centered[j] = x[[i, j]] - self.mean[j];
}
let mut w1 = vec![0.0_f64; self.n_components];
for c in 0..self.n_components {
let mut s = 0.0_f64;
for j in 0..d {
s += centered[j] * self.whitening[[c, j]];
}
w1[c] = s;
}
for c in 0..self.n_components {
let mut s = 0.0_f64;
for kk in 0..self.n_components {
s += self.components[[c, kk]] * w1[kk];
}
out[[i, c]] = s;
}
}
out
}
}
fn symmetric_decorrelate(w: &mut Array2<f64>) {
let n = w.nrows();
let mut a = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let mut s = 0.0_f64;
for k in 0..n {
s += w[[i, k]] * w[[j, k]];
}
a[[i, j]] = s;
}
}
let (eigvals, eigvecs) = jacobi_symmetric(&a, 300, 1e-12);
let mut d_half = Array2::<f64>::zeros((n, n));
for i in 0..n {
let inv = if eigvals[i] > 1e-12 {
1.0 / eigvals[i].sqrt()
} else {
0.0
};
d_half[[i, i]] = inv;
}
let mut tmp = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let mut s = 0.0_f64;
for k in 0..n {
s += eigvecs[[i, k]] * d_half[[k, k]] * eigvecs[[j, k]];
}
tmp[[i, j]] = s;
}
}
let mut new_w = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let mut s = 0.0_f64;
for k in 0..n {
s += tmp[[i, k]] * w[[k, j]];
}
new_w[[i, j]] = s;
}
}
*w = new_w;
}
fn lcg_next(state: &mut u64) -> u64 {
*state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
*state
}
fn uniform_f64(state: &mut u64) -> f64 {
(lcg_next(state) >> 11) as f64 / ((1u64 << 53) as f64)
}
fn uniform_symmetric(state: &mut u64, scale: f64) -> f64 {
(uniform_f64(state) - 0.5) * 2.0 * scale
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn fast_ica_runs_on_small_input() {
let x = array![
[1.0, 0.5],
[0.2, 0.9],
[-1.0, -0.4],
[0.3, -0.6],
[-0.7, 0.1],
[0.6, -0.9]
];
let ica = FastIca::fit_with(x.view(), 2, IcaFun::LogCosh, 100, 1e-4, 7).unwrap();
let s = ica.transform(x.view());
assert_eq!(s.dim(), (6, 2));
for v in s.iter() {
assert!(v.is_finite());
}
}
}