use crate::error::PaCMAPError;
use faer::{Mat, linalg::solvers::Svd};
pub fn pca_init(data: &[f32], n: usize, d: usize) -> Result<Vec<[f32; 2]>, PaCMAPError> {
debug_assert_eq!(data.len(), n * d);
let mut mean = vec![0.0_f32; d];
for row in data.chunks_exact(d) {
for (j, &v) in row.iter().enumerate() {
mean[j] += v;
}
}
for m in &mut mean {
*m /= n as f32;
}
let mut cov = Mat::<f32>::zeros(d, d);
for row in data.chunks_exact(d) {
for i in 0..d {
let xi = row[i] - mean[i];
for j in i..d {
let xj = row[j] - mean[j];
cov[(i, j)] += xi * xj;
}
}
}
let inv_n = 1.0 / n as f32;
for i in 0..d {
for j in i..d {
cov[(i, j)] *= inv_n;
if i != j {
cov[(j, i)] = cov[(i, j)];
}
}
}
let svd = Svd::<f32>::new(cov.as_ref())
.map_err(|e| PaCMAPError::Pca(format!("{e:?}")))?;
let u = svd.U();
let pc1: Vec<f32> = (0..d).map(|r| *u.get(r, 0)).collect();
let pc2: Vec<f32> = if d >= 2 { (0..d).map(|r| *u.get(r, 1)).collect() } else { vec![0.0; d] };
let mut embedding = Vec::with_capacity(n);
for row in data.chunks_exact(d) {
let mut s1 = 0.0_f32;
let mut s2 = 0.0_f32;
for j in 0..d {
let v = row[j] - mean[j];
s1 += v * pc1[j];
s2 += v * pc2[j];
}
embedding.push([s1, s2]);
}
Ok(embedding)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pca_separates_axis_aligned_clusters() {
let mut data: Vec<f32> = Vec::new();
for _ in 0..50 {
data.extend_from_slice(&[0.0_f32, 0.0]);
}
for _ in 0..50 {
data.extend_from_slice(&[10.0_f32, 0.0]);
}
let emb = pca_init(&data, 100, 2).unwrap();
let mean_left_x = emb[..50].iter().map(|p| p[0]).sum::<f32>() / 50.0;
let mean_right_x = emb[50..].iter().map(|p| p[0]).sum::<f32>() / 50.0;
assert!(
(mean_left_x - mean_right_x).abs() > 1.0,
"PCA should separate the two clusters along PC1"
);
}
}