use crate::error::{AprenderError, Result};
use crate::primitives::{Matrix, Vector};
#[derive(Debug, Clone)]
pub struct ICA {
n_components: usize,
max_iter: usize,
tol: f32,
random_state: Option<u64>,
whitening_matrix: Option<Matrix<f32>>,
unmixing_matrix: Option<Matrix<f32>>,
mean: Option<Vector<f32>>,
}
impl ICA {
#[must_use]
pub fn new(n_components: usize) -> Self {
Self {
n_components,
max_iter: 200,
tol: 1e-4,
random_state: None,
whitening_matrix: None,
unmixing_matrix: None,
mean: None,
}
}
#[must_use]
pub fn with_max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
#[must_use]
pub fn with_tolerance(mut self, tol: f32) -> Self {
self.tol = tol;
self
}
#[must_use]
pub fn with_random_state(mut self, seed: u64) -> Self {
self.random_state = Some(seed);
self
}
#[allow(clippy::similar_names)]
pub fn fit(&mut self, x: &Matrix<f32>) -> Result<()> {
let n = x.n_rows();
let p = x.n_cols();
if n == 0 || p == 0 {
return Err(AprenderError::Other("Data cannot be empty".into()));
}
if self.n_components > p {
return Err(AprenderError::Other(format!(
"n_components ({}) cannot exceed number of features ({})",
self.n_components, p
)));
}
let (x_centered, mean) = Self::center_data(x)?;
self.mean = Some(mean);
let (x_whitened, whitening_matrix) = Self::whiten_data(&x_centered, self.n_components)?;
self.whitening_matrix = Some(whitening_matrix);
let unmixing = self.fastica(&x_whitened)?;
self.unmixing_matrix = Some(unmixing);
Ok(())
}
pub fn transform(&self, x: &Matrix<f32>) -> Result<Matrix<f32>> {
let mean = self
.mean
.as_ref()
.ok_or_else(|| AprenderError::Other("Model not fitted. Call fit() first.".into()))?;
let whitening = self
.whitening_matrix
.as_ref()
.ok_or_else(|| AprenderError::Other("Model not fitted. Call fit() first.".into()))?;
let unmixing = self
.unmixing_matrix
.as_ref()
.ok_or_else(|| AprenderError::Other("Model not fitted. Call fit() first.".into()))?;
let n = x.n_rows();
let p = x.n_cols();
if p != mean.len() {
return Err(AprenderError::DimensionMismatch {
expected: format!("{} features", mean.len()),
actual: format!("{p} features in data"),
});
}
let mut x_centered_data = Vec::with_capacity(n * p);
for i in 0..n {
for j in 0..p {
x_centered_data.push(x.get(i, j) - mean[j]);
}
}
let x_centered = Matrix::from_vec(n, p, x_centered_data)
.map_err(|e| AprenderError::Other(format!("Centering failed: {e}")))?;
let x_whitened = x_centered
.matmul(whitening)
.map_err(|e| AprenderError::Other(format!("Whitening failed: {e}")))?;
x_whitened
.matmul(unmixing)
.map_err(|e| AprenderError::Other(format!("Unmixing failed: {e}")))
}
#[allow(clippy::needless_range_loop)]
fn center_data(x: &Matrix<f32>) -> Result<(Matrix<f32>, Vector<f32>)> {
let n = x.n_rows();
let p = x.n_cols();
let mut means = vec![0.0_f32; p];
#[allow(clippy::needless_range_loop)]
for j in 0..p {
let mut sum = 0.0;
for i in 0..n {
sum += x.get(i, j);
}
means[j] = sum / n as f32;
}
let mut centered_data = Vec::with_capacity(n * p);
for i in 0..n {
for j in 0..p {
centered_data.push(x.get(i, j) - means[j]);
}
}
let centered = Matrix::from_vec(n, p, centered_data)
.map_err(|e| AprenderError::Other(format!("Failed to center data: {e}")))?;
Ok((centered, Vector::from_vec(means)))
}
#[allow(clippy::similar_names)]
#[allow(clippy::needless_range_loop)]
fn whiten_data(
x_centered: &Matrix<f32>,
n_components: usize,
) -> Result<(Matrix<f32>, Matrix<f32>)> {
let n = x_centered.n_rows();
let p = x_centered.n_cols();
let xt = x_centered.transpose();
let cov = xt
.matmul(x_centered)
.map_err(|e| AprenderError::Other(format!("Covariance computation failed: {e}")))?;
let mut cov_data = vec![0.0_f32; p * p];
for i in 0..p {
for j in 0..p {
cov_data[i * p + j] = cov.get(i, j) / n as f32;
}
}
let cov_scaled = Matrix::from_vec(p, p, cov_data)
.map_err(|e| AprenderError::Other(format!("Covariance scaling failed: {e}")))?;
let (eigenvalues, eigenvectors) = Self::eigen_decomposition(&cov_scaled, n_components)?;
let mut whitening_data = Vec::with_capacity(p * n_components);
for j in 0..n_components {
let scale = 1.0 / eigenvalues[j].sqrt();
for i in 0..p {
whitening_data.push(eigenvectors.get(i, j) * scale);
}
}
let whitening_matrix = Matrix::from_vec(p, n_components, whitening_data)
.map_err(|e| AprenderError::Other(format!("Whitening matrix creation failed: {e}")))?;
let x_whitened = x_centered
.matmul(&whitening_matrix)
.map_err(|e| AprenderError::Other(format!("Data whitening failed: {e}")))?;
Ok((x_whitened, whitening_matrix))
}
#[allow(clippy::needless_range_loop)]
fn eigen_decomposition(matrix: &Matrix<f32>, k: usize) -> Result<(Vec<f32>, Matrix<f32>)> {
let n = matrix.n_rows();
if matrix.n_cols() != n {
return Err(AprenderError::Other(
"Eigendecomposition requires square matrix".into(),
));
}
let mut eigenvalues = Vec::with_capacity(k);
let mut eigenvectors_data = Vec::with_capacity(n * k);
let mut residual = matrix.clone();
for _ in 0..k {
let (eigenvalue, eigenvector) = Self::power_iteration(&residual, 100)?;
eigenvalues.push(eigenvalue);
eigenvectors_data.extend(eigenvector.as_slice());
let mut new_residual_data = vec![0.0_f32; n * n];
for i in 0..n {
for j in 0..n {
let deflation = eigenvalue * eigenvector[i] * eigenvector[j];
new_residual_data[i * n + j] = residual.get(i, j) - deflation;
}
}
residual = Matrix::from_vec(n, n, new_residual_data)
.map_err(|e| AprenderError::Other(format!("Deflation failed: {e}")))?;
}
let eigenvectors = Matrix::from_vec(n, k, eigenvectors_data).map_err(|e| {
AprenderError::Other(format!("Eigenvector matrix creation failed: {e}"))
})?;
Ok((eigenvalues, eigenvectors))
}
#[allow(clippy::needless_range_loop)]
fn power_iteration(matrix: &Matrix<f32>, max_iter: usize) -> Result<(f32, Vector<f32>)> {
let n = matrix.n_rows();
let mut v = vec![1.0_f32; n];
let norm = (v.iter().map(|x| x * x).sum::<f32>()).sqrt();
for val in &mut v {
*val /= norm;
}
let mut eigenvalue = 0.0;
for _ in 0..max_iter {
let mut v_new = vec![0.0_f32; n];
for i in 0..n {
let mut sum = 0.0;
for j in 0..n {
sum += matrix.get(i, j) * v[j];
}
v_new[i] = sum;
}
let norm = (v_new.iter().map(|x| x * x).sum::<f32>()).sqrt();
if norm < 1e-10 {
return Err(AprenderError::Other(
"Power iteration converged to zero vector".into(),
));
}
for val in &mut v_new {
*val /= norm;
}
eigenvalue = norm;
v = v_new;
}
Ok((eigenvalue, Vector::from_vec(v)))
}
#[allow(clippy::similar_names)]
#[allow(clippy::needless_range_loop)]
fn fastica_iteration_step(
x_white: &Matrix<f32>,
w: &[f32],
w_vectors: &[f32],
comp: usize,
tol: f32,
) -> Result<(Vec<f32>, bool)> {
let n = x_white.n_rows();
let p = x_white.n_cols();
let mut wtx = vec![0.0_f32; n];
for i in 0..n {
let mut sum = 0.0;
for j in 0..p {
sum += w[j] * x_white.get(i, j);
}
wtx[i] = sum;
}
let mut ex_g = vec![0.0_f32; p];
for j in 0..p {
let mut sum = 0.0;
for i in 0..n {
let g = wtx[i].tanh();
sum += x_white.get(i, j) * g;
}
ex_g[j] = sum / n as f32;
}
let mut eg_prime = 0.0;
for i in 0..n {
let tanh_val = wtx[i].tanh();
eg_prime += 1.0 - tanh_val * tanh_val;
}
eg_prime /= n as f32;
let mut w_new = vec![0.0_f32; p];
for j in 0..p {
w_new[j] = ex_g[j] - eg_prime * w[j];
}
for prev_comp in 0..comp {
let mut dot = 0.0;
for j in 0..p {
dot += w_new[j] * w_vectors[prev_comp * p + j];
}
for j in 0..p {
w_new[j] -= dot * w_vectors[prev_comp * p + j];
}
}
let norm = (w_new.iter().map(|x| x * x).sum::<f32>()).sqrt();
if norm < 1e-10 {
return Err(AprenderError::Other(
"FastICA failed: w converged to zero".into(),
));
}
for val in &mut w_new {
*val /= norm;
}
let mut dot = 0.0;
for j in 0..p {
dot += w[j] * w_new[j];
}
let converged = (1.0 - dot.abs()) < tol;
Ok((w_new, converged))
}
fn fastica(&self, x_white: &Matrix<f32>) -> Result<Matrix<f32>> {
let p = x_white.n_cols();
let mut w_vectors = Vec::with_capacity(p * p);
for comp in 0..p {
let mut w = vec![0.0_f32; p];
w[comp % p] = 1.0;
let norm = (w.iter().map(|x| x * x).sum::<f32>()).sqrt();
for val in &mut w {
*val /= norm;
}
for _iter in 0..self.max_iter {
let (w_new, converged) =
Self::fastica_iteration_step(x_white, &w, &w_vectors, comp, self.tol)?;
w = w_new;
if converged {
break;
}
}
w_vectors.extend(&w);
}
Matrix::from_vec(p, p, w_vectors)
.map_err(|e| AprenderError::Other(format!("Failed to create unmixing matrix: {e}")))
}
}
#[cfg(test)]
#[path = "ica_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "tests_ica_contract.rs"]
mod tests_ica_contract;