use scirs2_core::ndarray::{Array1, Array2, Axis};
use scirs2_linalg::{eigh, inv, svd};
use crate::error::{Result, TransformError};
fn sample_cov(x: &[Vec<f64>]) -> Result<Array2<f64>> {
let n = x.len();
if n < 2 {
return Err(TransformError::InvalidInput(
"Need at least 2 samples for covariance".to_string(),
));
}
let p = x[0].len();
if p == 0 {
return Err(TransformError::InvalidInput(
"Feature dimension must be > 0".to_string(),
));
}
let mut means = vec![0.0f64; p];
for row in x.iter() {
if row.len() != p {
return Err(TransformError::InvalidInput(
"All rows must have the same length".to_string(),
));
}
for (j, &v) in row.iter().enumerate() {
means[j] += v;
}
}
for m in means.iter_mut() {
*m /= n as f64;
}
let mut cov = Array2::<f64>::zeros((p, p));
for row in x.iter() {
for i in 0..p {
let ci = row[i] - means[i];
for j in 0..p {
let cj = row[j] - means[j];
cov[[i, j]] += ci * cj;
}
}
}
let scale = 1.0 / (n as f64 - 1.0);
cov.mapv_inplace(|v| v * scale);
Ok(cov)
}
fn cross_cov(x: &[Vec<f64>], y: &[Vec<f64>]) -> Result<Array2<f64>> {
let n = x.len();
if n != y.len() {
return Err(TransformError::InvalidInput(
"x and y must have the same number of rows".to_string(),
));
}
if n < 2 {
return Err(TransformError::InvalidInput(
"Need at least 2 samples".to_string(),
));
}
let p = x[0].len();
let q = y[0].len();
let mut xmeans = vec![0.0f64; p];
let mut ymeans = vec![0.0f64; q];
for (xrow, yrow) in x.iter().zip(y.iter()) {
if xrow.len() != p {
return Err(TransformError::InvalidInput(
"All x-rows must have the same length".to_string(),
));
}
if yrow.len() != q {
return Err(TransformError::InvalidInput(
"All y-rows must have the same length".to_string(),
));
}
for (j, &v) in xrow.iter().enumerate() {
xmeans[j] += v;
}
for (j, &v) in yrow.iter().enumerate() {
ymeans[j] += v;
}
}
for m in xmeans.iter_mut() {
*m /= n as f64;
}
for m in ymeans.iter_mut() {
*m /= n as f64;
}
let mut cc = Array2::<f64>::zeros((p, q));
for (xrow, yrow) in x.iter().zip(y.iter()) {
for i in 0..p {
let ci = xrow[i] - xmeans[i];
for j in 0..q {
let cj = yrow[j] - ymeans[j];
cc[[i, j]] += ci * cj;
}
}
}
let scale = 1.0 / (n as f64 - 1.0);
cc.mapv_inplace(|v| v * scale);
Ok(cc)
}
fn col_means(x: &[Vec<f64>]) -> Vec<f64> {
if x.is_empty() {
return vec![];
}
let p = x[0].len();
let mut means = vec![0.0f64; p];
for row in x.iter() {
for (j, &v) in row.iter().enumerate() {
means[j] += v;
}
}
let n = x.len() as f64;
for m in means.iter_mut() {
*m /= n;
}
means
}
fn add_ridge(a: &mut Array2<f64>, reg: f64) {
let n = a.nrows().min(a.ncols());
for i in 0..n {
a[[i, i]] += reg;
}
}
fn matrix_inv_sqrt(a: &Array2<f64>, reg: f64) -> Result<Array2<f64>> {
let mut a_reg = a.clone();
add_ridge(&mut a_reg, reg);
let (eigenvalues, eigenvectors) =
eigh(&a_reg.view(), None).map_err(|e| {
TransformError::ComputationError(format!("eigh failed in inv_sqrt: {e}"))
})?;
let p = a.nrows();
let mut d_inv_sqrt = Array2::<f64>::zeros((p, p));
for i in 0..p {
let ev = eigenvalues[i].max(reg);
d_inv_sqrt[[i, i]] = 1.0 / ev.sqrt();
}
let vd = eigenvectors.dot(&d_inv_sqrt);
Ok(vd.dot(&eigenvectors.t()))
}
#[derive(Debug, Clone)]
pub struct CCA {
pub n_components: usize,
pub reg: f64,
}
#[derive(Debug, Clone)]
pub struct CCAModel {
pub n_components: usize,
pub wx: Array2<f64>,
pub wy: Array2<f64>,
pub correlations: Vec<f64>,
pub x_mean: Vec<f64>,
pub y_mean: Vec<f64>,
}
impl Default for CCA {
fn default() -> Self {
CCA {
n_components: 2,
reg: 1e-6,
}
}
}
impl CCA {
pub fn new(n_components: usize, reg: f64) -> Self {
CCA { n_components, reg }
}
pub fn fit(&self, x: &[Vec<f64>], y: &[Vec<f64>]) -> Result<CCAModel> {
let n = x.len();
if n == 0 {
return Err(TransformError::InvalidInput("Empty dataset".to_string()));
}
if n != y.len() {
return Err(TransformError::InvalidInput(
"X and Y must have equal number of samples".to_string(),
));
}
let p = x[0].len();
let q = y[0].len();
let k = self.n_components.min(p).min(q);
if k == 0 {
return Err(TransformError::InvalidInput(
"n_components must be > 0".to_string(),
));
}
let x_mean = col_means(x);
let y_mean = col_means(y);
let cxx = sample_cov(x)?;
let cyy = sample_cov(y)?;
let cxy = cross_cov(x, y)?;
let cxx_inv_sqrt = matrix_inv_sqrt(&cxx, self.reg)?;
let cyy_inv_sqrt = matrix_inv_sqrt(&cyy, self.reg)?;
let t = cxx_inv_sqrt.dot(&cxy).dot(&cyy_inv_sqrt);
let (u_mat, sigma, vt_mat) = svd(&t.view(), true, None)
.map_err(|e| TransformError::ComputationError(format!("SVD failed in CCA: {e}")))?;
let u_k = u_mat.slice(scirs2_core::ndarray::s![.., ..k]).to_owned();
let vt_k = vt_mat.slice(scirs2_core::ndarray::s![..k, ..]).to_owned();
let correlations: Vec<f64> = (0..k).map(|i| sigma[i].min(1.0).max(-1.0)).collect();
let wx = cxx_inv_sqrt.dot(&u_k);
let wy = cyy_inv_sqrt.dot(&vt_k.t().to_owned());
Ok(CCAModel {
n_components: k,
wx,
wy,
correlations,
x_mean,
y_mean,
})
}
}
impl CCAModel {
pub fn transform_x(&self, x: &[Vec<f64>]) -> Result<Vec<Vec<f64>>> {
let n = x.len();
let p = self.wx.nrows();
let k = self.n_components;
let mut out = vec![vec![0.0f64; k]; n];
for (i, row) in x.iter().enumerate() {
if row.len() != p {
return Err(TransformError::InvalidInput(format!(
"Row {i}: expected {p} features, got {}",
row.len()
)));
}
let centered: Vec<f64> = row.iter().zip(self.x_mean.iter()).map(|(v, m)| v - m).collect();
for j in 0..k {
let s: f64 = centered.iter().enumerate().map(|(fi, &cv)| cv * self.wx[[fi, j]]).sum();
out[i][j] = s;
}
}
Ok(out)
}
pub fn transform_y(&self, y: &[Vec<f64>]) -> Result<Vec<Vec<f64>>> {
let n = y.len();
let q = self.wy.nrows();
let k = self.n_components;
let mut out = vec![vec![0.0f64; k]; n];
for (i, row) in y.iter().enumerate() {
if row.len() != q {
return Err(TransformError::InvalidInput(format!(
"Row {i}: expected {q} features, got {}",
row.len()
)));
}
let centered: Vec<f64> = row.iter().zip(self.y_mean.iter()).map(|(v, m)| v - m).collect();
for j in 0..k {
let s: f64 = centered.iter().enumerate().map(|(fi, &cv)| cv * self.wy[[fi, j]]).sum();
out[i][j] = s;
}
}
Ok(out)
}
pub fn canonical_correlations(&self) -> &[f64] {
&self.correlations
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum KernelType {
Linear,
Polynomial {
degree: u32,
gamma: f64,
coef0: f64,
},
RBF {
gamma: f64,
},
Sigmoid {
gamma: f64,
coef0: f64,
},
}
impl KernelType {
fn compute(&self, a: &[f64], b: &[f64]) -> f64 {
match self {
KernelType::Linear => a.iter().zip(b.iter()).map(|(ai, bi)| ai * bi).sum(),
KernelType::Polynomial { degree, gamma, coef0 } => {
let dot: f64 = a.iter().zip(b.iter()).map(|(ai, bi)| ai * bi).sum();
(gamma * dot + coef0).powi(*degree as i32)
}
KernelType::RBF { gamma } => {
let sq_dist: f64 = a.iter().zip(b.iter()).map(|(ai, bi)| (ai - bi).powi(2)).sum();
(-gamma * sq_dist).exp()
}
KernelType::Sigmoid { gamma, coef0 } => {
let dot: f64 = a.iter().zip(b.iter()).map(|(ai, bi)| ai * bi).sum();
(gamma * dot + coef0).tanh()
}
}
}
}
fn kernel_matrix(x: &[Vec<f64>], kernel: &KernelType) -> Array2<f64> {
let n = x.len();
let mut k = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in i..n {
let v = kernel.compute(&x[i], &x[j]);
k[[i, j]] = v;
k[[j, i]] = v;
}
}
k
}
fn center_kernel(k: &Array2<f64>) -> Array2<f64> {
let n = k.nrows();
let nf = n as f64;
let row_means = k.mean_axis(Axis(1)).unwrap_or_else(|| Array1::zeros(n));
let grand_mean = row_means.sum() / nf;
let mut kc = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
kc[[i, j]] = k[[i, j]] - row_means[i] - row_means[j] + grand_mean;
}
}
kc
}
#[derive(Debug, Clone)]
pub struct KCCA {
pub n_components: usize,
pub kernel_x: KernelType,
pub kernel_y: KernelType,
pub reg: f64,
}
#[derive(Debug, Clone)]
pub struct KCCAModel {
pub n_components: usize,
pub alpha: Array2<f64>,
pub beta: Array2<f64>,
pub correlations: Vec<f64>,
pub x_train: Vec<Vec<f64>>,
pub y_train: Vec<Vec<f64>>,
pub kernel_x: KernelType,
pub kernel_y: KernelType,
}
impl Default for KCCA {
fn default() -> Self {
KCCA {
n_components: 2,
kernel_x: KernelType::RBF { gamma: 1.0 },
kernel_y: KernelType::RBF { gamma: 1.0 },
reg: 1e-4,
}
}
}
impl KCCA {
pub fn new(
n_components: usize,
kernel_x: KernelType,
kernel_y: KernelType,
reg: f64,
) -> Self {
KCCA { n_components, kernel_x, kernel_y, reg }
}
pub fn fit(&self, x: &[Vec<f64>], y: &[Vec<f64>]) -> Result<KCCAModel> {
let n = x.len();
if n == 0 || n != y.len() {
return Err(TransformError::InvalidInput(
"Views must have equal non-zero number of samples".to_string(),
));
}
let k = self.n_components.min(n);
let kx = kernel_matrix(x, &self.kernel_x);
let ky = kernel_matrix(y, &self.kernel_y);
let kx_c = center_kernel(&kx);
let ky_c = center_kernel(&ky);
let mut kx_reg = kx_c.clone();
let mut ky_reg = ky_c.clone();
add_ridge(&mut kx_reg, self.reg * n as f64);
add_ridge(&mut ky_reg, self.reg * n as f64);
let kx_inv = inv(&kx_reg.view(), None)
.map_err(|e| TransformError::ComputationError(format!("KCCA: inv(K_X+rI) failed: {e}")))?;
let ky_inv = inv(&ky_reg.view(), None)
.map_err(|e| TransformError::ComputationError(format!("KCCA: inv(K_Y+rI) failed: {e}")))?;
let a_mat = kx_inv.dot(&kx_c); let b_mat = ky_inv.dot(&ky_c); let m_x = a_mat.dot(&ky_c).dot(&b_mat);
let max_power_iter = 200;
let tol = 1e-8;
let mut alphas = Array2::<f64>::zeros((n, k));
let mut correlations = Vec::with_capacity(k);
let mut m_deflated = m_x.clone();
for comp in 0..k {
let mut v = Array1::<f64>::zeros(n);
for i in 0..n {
v[i] = ((i + comp + 1) as f64 * 0.618).sin();
}
let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm > 1e-14 {
v.mapv_inplace(|x| x / norm);
}
let mut eigenvalue = 0.0_f64;
for _iter in 0..max_power_iter {
let v_new = m_deflated.dot(&v);
let new_norm: f64 = v_new.iter().map(|x| x * x).sum::<f64>().sqrt();
if new_norm < 1e-14 {
break;
}
let new_eigenvalue = v.dot(&v_new); let v_normed = &v_new / new_norm;
let diff: f64 = v_normed.iter().zip(v.iter()).map(|(a, b)| (a - b).powi(2)).sum::<f64>().sqrt();
v = v_normed;
eigenvalue = new_eigenvalue;
if diff < tol {
break;
}
}
let alpha_norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if alpha_norm > 1e-14 {
for i in 0..n {
alphas[[i, comp]] = v[i] / alpha_norm;
}
}
correlations.push(eigenvalue.abs().sqrt().min(1.0));
for i in 0..n {
for j in 0..n {
m_deflated[[i, j]] -= eigenvalue * v[i] * v[j];
}
}
}
let mut betas = Array2::<f64>::zeros((n, k));
for comp in 0..k {
let alpha_col = alphas.column(comp).to_owned();
let beta_col = b_mat.dot(&kx_c.dot(&alpha_col));
let beta_norm: f64 = beta_col.iter().map(|x| x * x).sum::<f64>().sqrt();
if beta_norm > 1e-14 {
for i in 0..n {
betas[[i, comp]] = beta_col[i] / beta_norm;
}
}
}
Ok(KCCAModel {
n_components: k,
alpha: alphas,
beta: betas,
correlations,
x_train: x.to_vec(),
y_train: y.to_vec(),
kernel_x: self.kernel_x.clone(),
kernel_y: self.kernel_y.clone(),
})
}
}
impl KCCAModel {
pub fn canonical_correlations(&self) -> &[f64] {
&self.correlations
}
pub fn transform_x(&self, x_new: &[Vec<f64>]) -> Result<Vec<Vec<f64>>> {
let n_new = x_new.len();
let n_train = self.x_train.len();
let k = self.n_components;
let mut kn = Array2::<f64>::zeros((n_new, n_train));
for (i, xi) in x_new.iter().enumerate() {
for (j, xj) in self.x_train.iter().enumerate() {
kn[[i, j]] = self.kernel_x.compute(xi, xj);
}
}
let mut out = vec![vec![0.0f64; k]; n_new];
for i in 0..n_new {
for j in 0..k {
let s: f64 = (0..n_train).map(|t| kn[[i, t]] * self.alpha[[t, j]]).sum();
out[i][j] = s;
}
}
Ok(out)
}
pub fn transform_y(&self, y_new: &[Vec<f64>]) -> Result<Vec<Vec<f64>>> {
let n_new = y_new.len();
let n_train = self.y_train.len();
let k = self.n_components;
let mut kn = Array2::<f64>::zeros((n_new, n_train));
for (i, yi) in y_new.iter().enumerate() {
for (j, yj) in self.y_train.iter().enumerate() {
kn[[i, j]] = self.kernel_y.compute(yi, yj);
}
}
let mut out = vec![vec![0.0f64; k]; n_new];
for i in 0..n_new {
for j in 0..k {
let s: f64 = (0..n_train).map(|t| kn[[i, t]] * self.beta[[t, j]]).sum();
out[i][j] = s;
}
}
Ok(out)
}
}
#[derive(Debug, Clone)]
pub struct GCCA {
pub n_components: usize,
pub reg: f64,
pub max_iter: usize,
pub tol: f64,
}
#[derive(Debug, Clone)]
pub struct GCCAModel {
pub n_components: usize,
pub projections: Vec<Array2<f64>>,
pub g: Array2<f64>,
pub view_means: Vec<Vec<f64>>,
}
impl Default for GCCA {
fn default() -> Self {
GCCA {
n_components: 2,
reg: 1e-4,
max_iter: 200,
tol: 1e-6,
}
}
}
impl GCCA {
pub fn new(n_components: usize, reg: f64) -> Self {
GCCA { n_components, reg, ..Default::default() }
}
pub fn fit(&self, views: &[&[Vec<f64>]]) -> Result<GCCAModel> {
let n_views = views.len();
if n_views < 2 {
return Err(TransformError::InvalidInput(
"GCCA requires at least 2 views".to_string(),
));
}
let n = views[0].len();
for (vi, view) in views.iter().enumerate() {
if view.len() != n {
return Err(TransformError::InvalidInput(format!(
"View {vi} has {} samples but view 0 has {n}",
view.len()
)));
}
}
let k = self.n_components.min(n);
let view_means: Vec<Vec<f64>> = views.iter().map(|v| col_means(v)).collect();
let mut view_arrays: Vec<Array2<f64>> = Vec::with_capacity(n_views);
for (vi, (view, means)) in views.iter().zip(view_means.iter()).enumerate() {
let p = view[0].len();
let mut arr = Array2::<f64>::zeros((n, p));
for (i, row) in view.iter().enumerate() {
if row.len() != p {
return Err(TransformError::InvalidInput(format!(
"View {vi} row {i} length mismatch"
)));
}
for (j, &v) in row.iter().enumerate() {
arr[[i, j]] = v - means[j];
}
}
view_arrays.push(arr);
}
let mut cov_inv: Vec<Array2<f64>> = Vec::with_capacity(n_views);
for arr in view_arrays.iter() {
let p = arr.ncols();
let mut c = Array2::<f64>::zeros((p, p));
for i in 0..n {
for a in 0..p {
for b in 0..p {
c[[a, b]] += arr[[i, a]] * arr[[i, b]];
}
}
}
let scale = 1.0 / (n as f64 - 1.0).max(1.0);
c.mapv_inplace(|v| v * scale);
add_ridge(&mut c, self.reg);
let ci = inv(&c.view(), None)
.map_err(|e| TransformError::ComputationError(format!("Cov inv failed: {e}")))?;
cov_inv.push(ci);
}
let mut g = Array2::<f64>::zeros((n, k));
{
let first = &view_arrays[0];
let (u, _s, _vt) = svd(&first.view(), true, None).map_err(|e| {
TransformError::ComputationError(format!("GCCA init SVD failed: {e}"))
})?;
let cols = k.min(u.ncols());
for i in 0..n {
for j in 0..cols {
g[[i, j]] = u[[i, j]];
}
}
}
for _iter in 0..self.max_iter {
let g_old = g.clone();
let mut g_new = Array2::<f64>::zeros((n, k));
for (arr, ci) in view_arrays.iter().zip(cov_inv.iter()) {
let xg = arr.t().dot(&g);
let w = ci.dot(&xg) / (n as f64);
let contrib = arr.dot(&w);
g_new = g_new + contrib;
}
let (u, _s, _vt) = svd(&g_new.view(), true, None).map_err(|e| {
TransformError::ComputationError(format!("GCCA power iter SVD: {e}"))
})?;
let cols = k.min(u.ncols());
g = Array2::<f64>::zeros((n, k));
for i in 0..n {
for j in 0..cols {
g[[i, j]] = u[[i, j]];
}
}
let diff: f64 = g.iter().zip(g_old.iter()).map(|(a, b)| (a - b).powi(2)).sum::<f64>().sqrt();
if diff < self.tol {
break;
}
}
let mut projections: Vec<Array2<f64>> = Vec::with_capacity(n_views);
for (arr, ci) in view_arrays.iter().zip(cov_inv.iter()) {
let xg = arr.t().dot(&g);
let w = ci.dot(&xg) / (n as f64);
projections.push(w);
}
Ok(GCCAModel {
n_components: k,
projections,
g,
view_means,
})
}
}
impl GCCAModel {
pub fn transform_view(&self, view: &[Vec<f64>], view_idx: usize) -> Result<Vec<Vec<f64>>> {
if view_idx >= self.projections.len() {
return Err(TransformError::InvalidInput(format!(
"view_idx {view_idx} out of range (have {} views)",
self.projections.len()
)));
}
let w = &self.projections[view_idx];
let means = &self.view_means[view_idx];
let p = w.nrows();
let k = self.n_components;
let n = view.len();
let mut out = vec![vec![0.0f64; k]; n];
for (i, row) in view.iter().enumerate() {
if row.len() != p {
return Err(TransformError::InvalidInput(format!(
"Row {i}: expected {p} features, got {}",
row.len()
)));
}
let centered: Vec<f64> = row.iter().zip(means.iter()).map(|(v, m)| v - m).collect();
for j in 0..k {
let s: f64 = (0..p).map(|fi| centered[fi] * w[[fi, j]]).sum();
out[i][j] = s;
}
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_correlated_views(n: usize) -> (Vec<Vec<f64>>, Vec<Vec<f64>>) {
let x: Vec<Vec<f64>> = (0..n)
.map(|i| {
let t = i as f64 * 0.1;
vec![t.sin(), t.cos(), t * 0.01]
})
.collect();
let y: Vec<Vec<f64>> = (0..n)
.map(|i| {
let t = i as f64 * 0.1;
vec![t.sin() + 0.05 * (i as f64).sin(), t.cos() * 1.1]
})
.collect();
(x, y)
}
#[test]
fn test_cca_basic() {
let (x, y) = make_correlated_views(80);
let cca = CCA::new(2, 1e-4);
let model = cca.fit(&x, &y).expect("CCA fit failed");
assert_eq!(model.n_components, 2);
assert_eq!(model.correlations.len(), 2);
for &c in &model.correlations {
assert!(c >= 0.0 && c <= 1.0 + 1e-9, "correlation {c} out of [0,1]");
}
let zx = model.transform_x(&x).expect("transform_x failed");
let zy = model.transform_y(&y).expect("transform_y failed");
assert_eq!(zx.len(), 80);
assert_eq!(zy.len(), 80);
assert_eq!(zx[0].len(), 2);
}
#[test]
fn test_cca_correlations_ordered() {
let (x, y) = make_correlated_views(100);
let cca = CCA::new(2, 1e-4);
let model = cca.fit(&x, &y).expect("CCA fit");
let corrs = model.canonical_correlations();
assert!(corrs[0] >= corrs[1] - 1e-9, "correlations should be non-increasing");
}
#[test]
fn test_kcca_basic() {
let (x, y) = make_correlated_views(40);
let kcca = KCCA::new(
2,
KernelType::RBF { gamma: 0.5 },
KernelType::RBF { gamma: 0.5 },
1e-2,
);
let model = kcca.fit(&x, &y).expect("KCCA fit failed");
assert_eq!(model.n_components, 2);
let zx = model.transform_x(&x).expect("KCCA transform_x");
assert_eq!(zx.len(), 40);
assert_eq!(zx[0].len(), 2);
}
#[test]
fn test_gcca_basic() {
let (x, y) = make_correlated_views(60);
let z: Vec<Vec<f64>> = x.iter().zip(y.iter()).map(|(xi, yi)| {
let mut v = xi.clone();
v.extend_from_slice(yi);
v
}).collect();
let gcca = GCCA::new(2, 1e-3);
let views: &[&[Vec<f64>]] = &[x.as_slice(), y.as_slice(), z.as_slice()];
let model = gcca.fit(views).expect("GCCA fit failed");
assert_eq!(model.n_components, 2);
assert_eq!(model.projections.len(), 3);
let embed = model.transform_view(&x, 0).expect("transform_view");
assert_eq!(embed.len(), 60);
assert_eq!(embed[0].len(), 2);
}
#[test]
fn test_cca_error_mismatched_rows() {
let x = vec![vec![1.0, 2.0]; 10];
let y = vec![vec![1.0]; 5];
let cca = CCA::default();
assert!(cca.fit(&x, &y).is_err());
}
}