use faer::{
Accum, Mat, MatRef, Par, Scale,
linalg::solvers::{PartialPivLu, Solve},
};
use rand::prelude::*;
use rand_distr::Distribution;
use rand_distr::Normal;
use rayon::prelude::*;
use crate::prelude::*;
use bixverse_core::core::math::matrix_helpers::scale_matrix_col;
use bixverse_core::core::math::pca_svd::randomised_svd;
use bixverse_core::utils::faer_parallelism;
use bixverse_core::utils::gemm::gemm;
type IcaRes<T> = (Mat<T>, T);
#[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,
}
}
#[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>), MethodsErrors> {
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| CoreErrors::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>, MethodsErrors> {
let svd_res = w
.thin_svd()
.map_err(|e| CoreErrors::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>, MethodsErrors> {
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 mut is_test = vec![false; no_samples];
for &idx in test_indices {
is_test[idx] = true;
}
let train_indices: Vec<usize> = indices
.iter()
.filter(|&&idx| !is_test[idx])
.cloned()
.collect();
let x_i = Mat::<T>::from_fn(train_indices.len(), no_features, |new_row, j| {
x[(train_indices[new_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,
})
}
}
fn fast_ica_core<T: BixverseFloat>(
par: Par,
x: MatRef<T>,
w_init: MatRef<T>,
tol: T,
maxit: usize,
verbose: bool,
contrast: impl Fn(T) -> (T, T),
) -> Result<IcaRes<T>, MethodsErrors> {
let p = x.ncols();
let mut w = update_mix_mat(w_init)?;
let n_comp = w.nrows();
let mut lim = vec![T::from_f64(1000.0).unwrap(); maxit];
let p_recip = T::from_usize(p).unwrap().recip();
let ones = Mat::<T>::from_fn(p, 1, |_, _| T::one());
let mut wx = Mat::<T>::zeros(n_comp, p);
let mut gwx = Mat::<T>::zeros(n_comp, p);
let mut gwx_2 = Mat::<T>::zeros(n_comp, p);
let mut v1 = Mat::<T>::zeros(n_comp, n_comp);
let mut row_sums = Mat::<T>::zeros(n_comp, 1);
let mut it = 0;
while it < maxit && lim[it] > tol {
gemm(wx.as_mut(), Accum::Replace, w.as_ref(), x, T::one(), par);
for j in 0..p {
let wx_col = wx.col_as_slice(j);
let gwx_col = gwx.col_as_slice_mut(j);
let gwx_2_col = gwx_2.col_as_slice_mut(j);
for ((&u, g), g2) in wx_col.iter().zip(gwx_col).zip(gwx_2_col) {
(*g, *g2) = contrast(u);
}
}
gemm(
v1.as_mut(),
Accum::Replace,
gwx.as_ref(),
x.transpose(),
p_recip,
par,
);
gemm(
row_sums.as_mut(),
Accum::Replace,
gwx_2.as_ref(),
ones.as_ref(),
T::one(),
par,
);
let v2 = Mat::<T>::from_fn(n_comp, n_comp, |i, j| {
row_sums[(i, 0)] * p_recip * w[(i, j)]
});
let w1 = update_mix_mat((v1.as_ref() - v2.as_ref()).as_ref())?;
let tol_it = (0..n_comp)
.map(|i| {
let d = (0..n_comp).fold(T::zero(), |acc, c| acc + w1[(i, c)] * w[(i, c)]);
(d.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))
}
fn logcosh_contrast<T: BixverseFloat>(alpha: T) -> impl Fn(T) -> (T, T) {
move |u| {
let g = (alpha * u).tanh();
(g, alpha * (T::one() - g * g))
}
}
fn exp_contrast<T: BixverseFloat>() -> impl Fn(T) -> (T, T) {
let neg_half = T::from_f64(-0.5).unwrap();
move |u| {
let e = (neg_half * u * u).exp();
(u * e, (T::one() - u * u) * e)
}
}
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>, MethodsErrors> {
fast_ica_core(
faer_parallelism(),
x,
w_init,
tol,
maxit,
verbose,
logcosh_contrast(alpha),
)
}
pub fn fast_ica_exp<T: BixverseFloat>(
x: MatRef<T>,
w_init: MatRef<T>,
tol: T,
maxit: usize,
verbose: bool,
) -> Result<IcaRes<T>, MethodsErrors> {
fast_ica_core(
faer_parallelism(),
x,
w_init,
tol,
maxit,
verbose,
exp_contrast(),
)
}
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>), MethodsErrors> {
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_core(
Par::Seq,
x_whiten.as_ref(),
w_init.as_ref(),
ica_params.tol,
ica_params.maxit,
ica_params.verbose,
exp_contrast(),
),
IcaType::LogCosh => fast_ica_core(
Par::Seq,
x_whiten.as_ref(),
w_init.as_ref(),
ica_params.tol,
ica_params.maxit,
ica_params.verbose,
logcosh_contrast(ica_params.alpha),
),
})
.collect::<Result<Vec<(Mat<T>, T)>, MethodsErrors>>()?;
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 mut w = Mat::<T>::zeros(a.nrows(), k_red.ncols());
gemm(
w.as_mut(),
Accum::Replace,
a.as_ref(),
k_red,
T::one(),
Par::Seq,
);
let mut to_solve = Mat::<T>::zeros(w.nrows(), w.nrows());
gemm(
to_solve.as_mut(),
Accum::Replace,
w.as_ref(),
w.transpose(),
T::one(),
Par::Seq,
);
let identity = Mat::<T>::identity(to_solve.nrows(), to_solve.ncols());
let lu = PartialPivLu::new(to_solve.as_ref());
let solved = lu.solve(&identity);
let mut out = Mat::<T>::zeros(w.ncols(), solved.ncols());
gemm(
out.as_mut(),
Accum::Replace,
w.transpose(),
solved.as_ref(),
T::one(),
Par::Seq,
);
out
})
.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>), MethodsErrors> {
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>)>, MethodsErrors>>()?;
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))
}