use faer::{
Mat, MatRef, Scale,
linalg::solvers::{PartialPivLu, Solve},
};
use rand::prelude::*;
use rand_distr::Distribution;
use rand_distr::Normal;
use rayon::prelude::*;
use crate::core::math::matrix_helpers::scale_matrix_col;
use crate::core::math::pca_svd::randomised_svd;
use crate::prelude::*;
#[derive(Clone, Debug, Default)]
pub enum IcaType {
#[default]
LogCosh,
Exp,
}
pub fn parse_ica_type(s: &str) -> Option<IcaType> {
match s.to_lowercase().as_str() {
"exp" => Some(IcaType::Exp),
"logcosh" => Some(IcaType::LogCosh),
_ => None,
}
}
type IcaRes<T> = (Mat<T>, T);
#[derive(Clone, Debug)]
pub struct IcaParams<T: BixverseFloat> {
pub maxit: usize,
pub alpha: T,
pub tol: T,
pub verbose: bool,
}
pub fn prepare_whitening<T: BixverseFloat>(
x: MatRef<T>,
fast_svd: bool,
seed: usize,
rank: usize,
oversampling: Option<usize>,
n_power_iter: Option<usize>,
) -> Result<(Mat<T>, Mat<T>), BixverseErrors> {
let n = x.nrows();
let centered = scale_matrix_col(&x, false);
let centered = centered.transpose();
let n_recip = T::from_usize(n).unwrap().recip();
let v = Scale(n_recip) * (centered * centered.transpose());
let k = if fast_svd {
let svd_res = randomised_svd(v.as_ref(), rank, seed, oversampling, n_power_iter)?;
let s: Vec<T> = svd_res.s.iter().map(|x| x.sqrt().recip()).collect();
let d = faer_diagonal_from_vec(s);
d * svd_res.u.transpose()
} else {
let svd_res = v
.thin_svd()
.map_err(|e| BixverseErrors::FaerSvdError(format!("{e:?}")))?;
let s = svd_res
.S()
.column_vector()
.iter()
.map(|x| x.sqrt().recip())
.collect::<Vec<_>>();
let d = faer_diagonal_from_vec(s);
let u = svd_res.U();
let u_t = u.transpose();
d * u_t
};
Ok((centered.cloned(), k))
}
fn update_mix_mat<T: BixverseFloat>(w: MatRef<T>) -> Result<Mat<T>, BixverseErrors> {
let svd_res = w
.thin_svd()
.map_err(|e| BixverseErrors::FaerSvdError(format!("{e:?}")))?;
let s = svd_res.S();
let u = svd_res.U();
let s = s
.column_vector()
.iter()
.map(|x| x.recip())
.collect::<Vec<_>>();
let d = faer_diagonal_from_vec(s);
Ok(u * d * u.transpose() * w)
}
fn create_w_init<T: BixverseFloat>(n_comp: usize, seed: u64) -> Mat<T> {
let mut rng = StdRng::seed_from_u64(seed);
let normal = Normal::new(0.0, 1.0).unwrap();
let vec_size = n_comp.pow(2);
let data: Vec<T> = (0..vec_size)
.map(|_| T::from_f64(normal.sample(&mut rng)).unwrap())
.collect();
Mat::from_fn(n_comp, n_comp, |i, j| data[i + j * n_comp])
}
pub struct IcaCvData<T: BixverseFloat> {
pub pre_white_matrices: Vec<Mat<T>>,
pub k_matrices: Vec<Mat<T>>,
}
impl<T: BixverseFloat> IcaCvData<T> {
pub fn create_from_data(
x: MatRef<T>,
num_folds: usize,
seed: usize,
rank: Option<usize>,
) -> Result<IcaCvData<T>, BixverseErrors> {
let no_samples = x.nrows();
let no_features = x.ncols();
let mut indices: Vec<usize> = (0..no_samples).collect();
let mut rng = StdRng::seed_from_u64(seed as u64);
indices.shuffle(&mut rng);
let svd_rank = rank.unwrap_or(no_features);
let fold_size = no_samples / num_folds;
let remainder = no_samples % num_folds;
let mut folds = Vec::with_capacity(num_folds);
let mut start = 0;
for idx in 0..num_folds {
let current_fold_size = if idx < remainder {
fold_size + 1
} else {
fold_size
};
let end = start + current_fold_size;
folds.push(indices[start..end].to_vec());
start = end;
}
let k_x_matrices: Vec<(Mat<T>, Mat<T>)> = folds
.par_iter()
.map(|test_indices| {
let train_indices: Vec<usize> = indices
.iter()
.filter(|&idx| !test_indices.contains(idx))
.cloned()
.collect();
let mut x_i = Mat::<T>::zeros(train_indices.len(), no_features);
for (new_row, old_row) in train_indices.iter().enumerate() {
for j in 0..no_features {
x_i[(new_row, j)] = x[(*old_row, j)];
}
}
prepare_whitening(x_i.as_ref(), true, seed + 1, svd_rank, None, None)
})
.collect::<Result<Vec<_>, _>>()?;
let mut pre_white_matrices = Vec::with_capacity(num_folds);
let mut k_matrices = Vec::with_capacity(num_folds);
for (x_i, k_i) in k_x_matrices {
pre_white_matrices.push(x_i);
k_matrices.push(k_i);
}
Ok(Self {
pre_white_matrices,
k_matrices,
})
}
}
pub fn fast_ica_logcosh<T: BixverseFloat>(
x: MatRef<T>,
w_init: MatRef<T>,
tol: T,
alpha: T,
maxit: usize,
verbose: bool,
) -> Result<IcaRes<T>, BixverseErrors> {
let p = x.ncols();
let mut w = update_mix_mat(w_init)?;
let mut lim = vec![T::from_f64(1000.0).unwrap(); maxit];
let mut it = 0;
while it < maxit && lim[it] > tol {
let wx: Mat<T> = w.clone() * x;
let gwx = Mat::from_fn(wx.nrows(), wx.ncols(), |i, j| {
let x = wx.get(i, j);
(alpha * *x).tanh()
});
let p_recip = T::from_usize(p).unwrap().recip();
let v1 = Scale(p_recip) * (gwx.clone() * x.transpose());
let gwx_2 = Mat::from_fn(gwx.nrows(), gwx.ncols(), |i, j| {
let x = gwx.get(i, j);
alpha * (T::one() - *x * *x)
});
let ones = Mat::from_fn(p, 1, |_, _| T::one());
let row_means = Scale(p_recip) * (gwx_2.clone() * ones);
let row_means_vec: Vec<T> = row_means
.as_ref()
.col_iter()
.flat_map(|col| col.iter())
.copied()
.collect();
let v2 = faer_diagonal_from_vec(row_means_vec) * w.clone();
let w1 = update_mix_mat((v1 - v2).as_ref())?;
let w1_up = w1.clone() * w.transpose();
let tol_it = w1_up
.diagonal()
.column_vector()
.iter()
.map(|x| (x.abs() - T::one()).abs())
.fold(T::neg_infinity(), |a, b| a.max(b));
if it + 1 < maxit {
lim[it + 1] = tol_it
}
if verbose {
println!("Iteration: {:?}, tol: {:?}", it + 1, tol_it)
}
w = w1;
it += 1;
}
let min_tol = array_min(&lim);
Ok((w, min_tol))
}
pub fn fast_ica_exp<T: BixverseFloat>(
x: MatRef<T>,
w_init: MatRef<T>,
tol: T,
maxit: usize,
verbose: bool,
) -> Result<IcaRes<T>, BixverseErrors> {
let p = x.ncols();
let mut w = update_mix_mat(w_init)?;
let mut lim = vec![T::from_f64(1000.0).unwrap(); maxit];
let mut it = 0;
while it < maxit && lim[it] > tol {
let wx: Mat<T> = w.clone() * x;
let neg_half = T::from_f64(-0.5).unwrap();
let gwx = Mat::from_fn(wx.nrows(), wx.ncols(), |i, j| {
let x = wx.get(i, j);
*x * (neg_half * *x * *x).exp()
});
let p_recip = T::from_usize(p).unwrap().recip();
let v1 = Scale(p_recip) * (gwx.clone() * x.transpose());
let gwx_2 = Mat::from_fn(wx.nrows(), wx.ncols(), |i, j| {
let x = wx.get(i, j);
(T::one() - *x * *x) * (neg_half * *x * *x).exp()
});
let ones = Mat::from_fn(p, 1, |_, _| T::one());
let row_means = Scale(p_recip) * (gwx_2.clone() * ones);
let row_means_vec: Vec<T> = row_means
.as_ref()
.col_iter()
.flat_map(|col| col.iter())
.copied()
.collect();
let v2 = faer_diagonal_from_vec(row_means_vec) * w.clone();
let w1 = update_mix_mat((v1 - v2).as_ref())?;
let w1_up = w1.clone() * w.transpose();
let tol_it = w1_up
.diagonal()
.column_vector()
.iter()
.map(|x| (x.abs() - T::one()).abs())
.fold(T::neg_infinity(), |a, b| a.max(b));
if it + 1 < maxit {
lim[it + 1] = tol_it
}
if verbose {
println!("Iteration: {:?}, tol: {:?}", it + 1, tol_it)
}
w = w1;
it += 1;
}
let min_tol = array_min(&lim);
Ok((w, min_tol))
}
pub fn stabilised_ica_iters<T: BixverseFloat>(
x_pre_whiten: MatRef<T>,
k: MatRef<T>,
no_comp: usize,
no_iters: usize,
ica_type: &str,
ica_params: IcaParams<T>,
random_seed: usize,
) -> Result<(Mat<T>, Vec<bool>), BixverseErrors> {
let w_inits: Vec<Mat<T>> = (0..no_iters)
.map(|iter| create_w_init(no_comp, (random_seed + iter) as u64))
.collect();
let k_ncol = k.ncols();
let k_red = k.get(0..no_comp, 0..k_ncol);
let x_whiten = k_red * x_pre_whiten;
let ica_type = parse_ica_type(ica_type).unwrap();
let iter_res: Vec<(Mat<T>, T)> = w_inits
.par_iter()
.map(|w_init| match ica_type {
IcaType::Exp => fast_ica_exp(
x_whiten.as_ref(),
w_init.as_ref(),
ica_params.tol,
ica_params.maxit,
ica_params.verbose,
),
IcaType::LogCosh => fast_ica_logcosh(
x_whiten.as_ref(),
w_init.as_ref(),
ica_params.tol,
ica_params.alpha,
ica_params.maxit,
ica_params.verbose,
),
})
.collect::<Result<Vec<(Mat<T>, T)>, BixverseErrors>>()?;
let mut convergence = Vec::new();
let mut a_matrices = Vec::new();
for (a, final_tol) in iter_res {
a_matrices.push(a);
convergence.push(final_tol < ica_params.tol);
}
let s_matrices: Vec<Mat<T>> = a_matrices
.par_iter()
.map(|a| {
let w = a.clone() * k_red;
let to_solve = w.clone() * w.transpose();
let identity = Mat::<T>::identity(to_solve.nrows(), to_solve.ncols());
let lu = PartialPivLu::new(to_solve.as_ref());
let solved = lu.solve(&identity);
w.transpose() * solved
})
.collect();
let s_combined = colbind_matrices(&s_matrices);
Ok((s_combined, convergence))
}
#[allow(clippy::too_many_arguments)]
pub fn stabilised_ica_cv<T: BixverseFloat>(
x: MatRef<T>,
no_comp: usize,
no_folds: usize,
no_iters: usize,
ica_type: &str,
ica_params: IcaParams<T>,
ica_cv_data: Option<IcaCvData<T>>,
seed: usize,
) -> Result<(Mat<T>, Vec<bool>), BixverseErrors> {
let cv_data = match ica_cv_data {
Some(data) => Ok(data), None => IcaCvData::create_from_data(x, no_folds, seed, Some(no_comp)),
}?;
let cv_res: Vec<(Mat<T>, Vec<bool>)> = cv_data
.k_matrices
.par_iter()
.zip(cv_data.pre_white_matrices)
.map(|(k_i, x_i)| {
stabilised_ica_iters(
x_i.as_ref(),
k_i.as_ref(),
no_comp,
no_iters,
ica_type,
ica_params.clone(),
seed + 2,
)
})
.collect::<Result<Vec<(Mat<T>, Vec<bool>)>, BixverseErrors>>()?;
let mut s_final = Vec::new();
let mut converged_final = Vec::new();
for (s_i, converged_i) in cv_res {
s_final.push(s_i);
converged_final.push(converged_i);
}
let s_final = colbind_matrices(&s_final);
let converged_final = flatten_vector(converged_final);
Ok((s_final, converged_final))
}