use nalgebra::DMatrix;
use rayon::prelude::*;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct EmbeddingGeometry {
pub n_rows: usize,
pub h: usize,
pub common_mode_cos: f32,
pub mean_pairwise_cos: f32,
pub eff_rank_raw: f32,
pub eff_rank_centered: f32,
pub eff_rank_double_centered: f32,
pub max_abs_corr: f32,
pub max_vif: f32,
}
fn participation_ratio(gram: &DMatrix<f64>) -> f32 {
let (s1, s2) = (gram.trace(), gram.norm_squared());
if s2 <= 0.0 {
return 0.0;
}
((s1 * s1) / s2) as f32
}
fn corr_and_vif(ctr_gram: &DMatrix<f64>) -> (f32, f32) {
let h = ctr_gram.nrows();
let inv_sd: Vec<f64> = (0..h)
.map(|j| {
let sd = ctr_gram[(j, j)].max(0.0).sqrt();
if sd > 0.0 {
1.0 / sd
} else {
0.0
}
})
.collect();
let corr = DMatrix::<f64>::from_fn(h, h, |i, j| {
if i == j {
1.0
} else {
ctr_gram[(i, j)] * inv_sd[i] * inv_sd[j]
}
});
let cr = &corr;
let max_abs_corr = (0..h)
.flat_map(|i| ((i + 1)..h).map(move |j| cr[(i, j)].abs()))
.fold(0.0f64, f64::max);
let eye = DMatrix::<f64>::identity(h, h);
let max_vif = corr
.clone()
.cholesky()
.map(|c| c.solve(&eye))
.or_else(|| corr.lu().solve(&eye))
.map_or(f32::INFINITY, |inv| {
(0..h).fold(0.0f64, |acc, j| acc.max(inv[(j, j)])) as f32
})
.max(1.0);
(max_abs_corr as f32, max_vif)
}
struct RowPass {
gram_upper: Vec<f64>,
col_sum: Vec<f64>,
unit_sum: Vec<f64>,
live: usize,
}
impl RowPass {
fn zero(h: usize) -> Self {
Self {
gram_upper: vec![0.0; h * h],
col_sum: vec![0.0; h],
unit_sum: vec![0.0; h],
live: 0,
}
}
fn add_row(mut self, row: &[f64]) -> Self {
let h = row.len();
for (i, &ri) in row.iter().enumerate() {
self.col_sum[i] += ri;
let upper = &mut self.gram_upper[i * h + i..(i + 1) * h];
for (g, &rj) in upper.iter_mut().zip(&row[i..]) {
*g += ri * rj;
}
}
let nrm = row.iter().map(|x| x * x).sum::<f64>().sqrt();
if nrm > 0.0 {
for (u, &r) in self.unit_sum.iter_mut().zip(row) {
*u += r / nrm;
}
self.live += 1;
}
self
}
fn merge(mut self, other: Self) -> Self {
for (a, b) in self.gram_upper.iter_mut().zip(other.gram_upper) {
*a += b;
}
for (a, b) in self.col_sum.iter_mut().zip(other.col_sum) {
*a += b;
}
for (a, b) in self.unit_sum.iter_mut().zip(other.unit_sum) {
*a += b;
}
self.live += other.live;
self
}
}
#[must_use]
pub fn embedding_geometry(e: &DMatrix<f32>) -> EmbeddingGeometry {
let (n, h) = (e.nrows(), e.ncols());
if n == 0 || h == 0 {
return EmbeddingGeometry {
n_rows: n,
h,
..Default::default()
};
}
let mut rm = vec![0.0f64; n * h];
rm.par_chunks_mut(h).enumerate().for_each(|(i, dst)| {
for (j, d) in dst.iter_mut().enumerate() {
*d = f64::from(e[(i, j)]);
}
});
let inv_n = 1.0 / n as f64;
let pass = rm
.par_chunks(h)
.fold(|| RowPass::zero(h), RowPass::add_row)
.reduce(|| RowPass::zero(h), RowPass::merge);
let raw_gram = DMatrix::<f64>::from_fn(h, h, |i, j| {
let (a, b) = if i <= j { (i, j) } else { (j, i) };
pass.gram_upper[a * h + b] * inv_n
});
let mean: Vec<f64> = pass.col_sum.iter().map(|c| c * inv_n).collect();
let ctr_gram = &raw_gram - DMatrix::<f64>::from_fn(h, h, |i, j| mean[i] * mean[j]);
let m = pass.live;
let mean_pairwise_cos = if m < 2 {
0.0
} else {
let s2: f64 = pass.unit_sum.iter().map(|x| x * x).sum();
let m = m as f64;
((s2 - m) / (m * (m - 1.0))) as f32
};
let mean_norm = mean.iter().map(|x| x * x).sum::<f64>().sqrt();
let common_mode_cos = if mean_norm <= 0.0 {
0.0
} else {
let acc: f64 = rm
.par_chunks(h)
.map(|row| {
let nrm = row.iter().map(|x| x * x).sum::<f64>().sqrt();
if nrm <= 0.0 {
return 0.0;
}
let dot: f64 = row.iter().zip(&mean).map(|(r, m)| r * m).sum();
(dot / (nrm * mean_norm)).abs()
})
.sum();
(acc * inv_n) as f32
};
let (max_abs_corr, max_vif) = corr_and_vif(&ctr_gram);
let proj = DMatrix::<f64>::identity(h, h) - DMatrix::<f64>::repeat(h, h, 1.0 / h as f64);
let double_gram = &proj * &ctr_gram * &proj;
EmbeddingGeometry {
n_rows: n,
h,
common_mode_cos,
mean_pairwise_cos,
eff_rank_raw: participation_ratio(&raw_gram),
eff_rank_centered: participation_ratio(&ctr_gram),
eff_rank_double_centered: participation_ratio(&double_gram),
max_abs_corr,
max_vif,
}
}
#[cfg(test)]
mod tests;