use crate::utils::gemm;
use faer::Accum;
use faer::linalg::matmul::triangular::BlockStructure;
use faer::{Mat, MatRef, Par};
use rayon::prelude::*;
use crate::prelude::*;
use crate::utils::faer_parallelism;
const GRAM_ROW_TILE: usize = 512;
#[derive(Debug, Clone, Copy, Default)]
pub enum PertDistance {
#[default]
Euclidean,
SquaredEuclidean,
}
pub fn parse_perturbation_distance(s: &str) -> Option<PertDistance> {
match s.to_lowercase().as_str() {
"euclidean" => Some(PertDistance::Euclidean),
"squared_euclidean" | "sqeuclidean" => Some(PertDistance::SquaredEuclidean),
_ => None,
}
}
fn row_norms_squared<T>(x: MatRef<T>) -> Vec<T>
where
T: BixverseFloat,
{
let mut out = vec![T::zero(); x.nrows()];
out.par_chunks_mut(GRAM_ROW_TILE)
.enumerate()
.for_each(|(c, chunk)| {
let start = c * GRAM_ROW_TILE;
for j in 0..x.ncols() {
let col = x.col(j).subrows(start, chunk.len());
for (o, &v) in chunk.iter_mut().zip(col.iter()) {
*o += v * v;
}
}
});
out
}
#[inline(always)]
fn finish_dist<T: BixverseFloat>(d_sq: T, dist: PertDistance) -> T {
match dist {
PertDistance::Euclidean => d_sq.sqrt(),
PertDistance::SquaredEuclidean => d_sq,
}
}
pub fn mean_pairwise_within<T>(x: MatRef<T>, dist: PertDistance) -> T
where
T: BixverseFloat + std::iter::Sum,
{
mean_pairwise_within_impl(x, dist, true)
}
fn mean_pairwise_within_impl<T>(x: MatRef<T>, dist: PertDistance, parallel: bool) -> T
where
T: BixverseFloat + std::iter::Sum,
{
let n = x.nrows();
if n < 2 {
return T::zero();
}
let mut gram = Mat::<T>::zeros(n, n);
gemm::gram(
gram.as_mut(),
BlockStructure::TriangularLower,
Accum::Replace,
x,
x.transpose(),
T::one(),
if parallel {
faer_parallelism()
} else {
Par::Seq
},
);
let norms_sq: Vec<T> = (0..n).map(|i| *gram.get(i, i)).collect();
let two = T::from_f64(2.0).unwrap();
let col_sum = |j: usize| -> T {
let mut s = T::zero();
for (i, &g) in gram.col(j).iter().enumerate().skip(j + 1) {
let d_sq = (norms_sq[i] + norms_sq[j] - two * g).max(T::zero());
s += finish_dist(d_sq, dist);
}
s
};
let total: T = if parallel {
(0..n).into_par_iter().map(col_sum).sum()
} else {
(0..n).map(col_sum).sum()
};
let n_pairs = T::from_usize(n * (n - 1) / 2).unwrap();
total / n_pairs
}
pub fn mean_pairwise_between<T>(
x: MatRef<T>,
y: MatRef<T>,
dist: PertDistance,
) -> Result<T, CoreErrors>
where
T: BixverseFloat + std::iter::Sum,
{
mean_pairwise_between_impl(x, y, dist, true)
}
fn mean_pairwise_between_impl<T>(
x: MatRef<T>,
y: MatRef<T>,
dist: PertDistance,
parallel: bool,
) -> Result<T, CoreErrors>
where
T: BixverseFloat + std::iter::Sum,
{
if x.ncols() != y.ncols() {
return Err(CoreErrors::NonMatchingFeatureDim {
dim_x: x.ncols(),
dim_y: y.ncols(),
});
}
let n_x = x.nrows();
let n_y = y.nrows();
if n_x == 0 || n_y == 0 {
return Ok(T::zero());
}
let x_norms = row_norms_squared(x);
let y_norms = row_norms_squared(y);
let two = T::from_f64(2.0).unwrap();
let par = if parallel {
faer_parallelism()
} else {
Par::Seq
};
let mut gram = Mat::<T>::zeros(GRAM_ROW_TILE.min(n_x), n_y);
let mut total = T::zero();
for start in (0..n_x).step_by(GRAM_ROW_TILE) {
let rows = GRAM_ROW_TILE.min(n_x - start);
let mut tile = gram.as_mut().submatrix_mut(0, 0, rows, n_y);
gemm::gemm(
tile.as_mut(),
Accum::Replace,
x.subrows(start, rows),
y.transpose(),
T::one(),
par,
);
let tile = tile.as_ref();
let x_tile = &x_norms[start..start + rows];
let col_sum = |j: usize| -> T {
let mut s = T::zero();
for (&g, &xn) in tile.col(j).iter().zip(x_tile) {
let d_sq = (xn + y_norms[j] - two * g).max(T::zero());
s += finish_dist(d_sq, dist);
}
s
};
let tile_total: T = if parallel {
(0..n_y).into_par_iter().map(col_sum).sum()
} else {
(0..n_y).map(col_sum).sum()
};
total += tile_total;
}
let n_pairs = T::from_usize(n_x * n_y).unwrap();
Ok(total / n_pairs)
}
fn group_rows_by_label<T>(embedding: MatRef<T>, labels: &[usize]) -> Result<Vec<Mat<T>>, CoreErrors>
where
T: BixverseFloat,
{
if embedding.nrows() != labels.len() {
return Err(CoreErrors::NumberLabelsNotEqualSampleNumber {
label_length: labels.len(),
n_samples: embedding.nrows(),
});
}
let d = embedding.ncols();
let n_groups = labels.iter().max().map(|&x| x + 1).unwrap_or(0);
let mut indices: Vec<Vec<usize>> = vec![Vec::new(); n_groups];
for (sample, &g) in labels.iter().enumerate() {
indices[g].push(sample);
}
let res = indices
.into_par_iter()
.map(|rows| Mat::from_fn(rows.len(), d, |i, j| *embedding.get(rows[i], j)))
.collect();
Ok(res)
}
pub fn edistance_two_matrices<T>(
x: MatRef<T>,
y: MatRef<T>,
dist: PertDistance,
) -> Result<T, CoreErrors>
where
T: BixverseFloat + std::iter::Sum,
{
let within_x = mean_pairwise_within(x, dist);
let within_y = mean_pairwise_within(y, dist);
let between = mean_pairwise_between(x, y, dist)?;
let two = T::from_f64(2.0).unwrap();
Ok(two * between - within_x - within_y)
}
pub(crate) fn edistance_two_matrices_seq<T>(
x: MatRef<T>,
y: MatRef<T>,
dist: PertDistance,
) -> Result<T, CoreErrors>
where
T: BixverseFloat + std::iter::Sum,
{
let within_x = mean_pairwise_within_impl(x, dist, false);
let within_y = mean_pairwise_within_impl(y, dist, false);
let between = mean_pairwise_between_impl(x, y, dist, false)?;
let two = T::from_f64(2.0).unwrap();
Ok(two * between - within_x - within_y)
}
pub fn pairwise_edistance<T>(
embedding: MatRef<T>,
labels: &[usize],
dist: PertDistance,
) -> Result<Mat<T>, CoreErrors>
where
T: BixverseFloat + std::iter::Sum,
{
let groups = group_rows_by_label(embedding, labels)?;
let n_groups = groups.len();
let inner_par = groups.len() < rayon::current_num_threads();
let withins: Vec<T> = groups
.par_iter()
.map(|g| mean_pairwise_within_impl(g.as_ref(), dist, inner_par))
.collect();
let pairs: Vec<(usize, usize)> = (0..n_groups)
.flat_map(|i| (i + 1..n_groups).map(move |j| (i, j)))
.collect();
let two = T::from_f64(2.0).unwrap();
let results: Vec<(usize, usize, T)> = pairs
.par_iter()
.map(|&(i, j)| {
let between = mean_pairwise_between_impl(
groups[i].as_ref(),
groups[j].as_ref(),
dist,
inner_par,
)?;
let ed = two * between - withins[i] - withins[j];
Ok((i, j, ed))
})
.collect::<Result<Vec<_>, CoreErrors>>()?;
let mut result = Mat::<T>::zeros(n_groups, n_groups);
for (i, j, val) in results {
result[(i, j)] = val;
result[(j, i)] = val;
}
Ok(result)
}
pub fn onesided_edistance<T>(
embedding: MatRef<T>,
labels: &[usize],
reference: usize,
dist: PertDistance,
) -> Result<Vec<T>, CoreErrors>
where
T: BixverseFloat + std::iter::Sum,
{
let groups = group_rows_by_label(embedding, labels)?;
let n_groups = groups.len();
assert!(reference < n_groups, "reference label out of range");
let inner_par = n_groups < rayon::current_num_threads();
let within_ref = mean_pairwise_within(groups[reference].as_ref(), dist);
let two = T::from_f64(2.0).unwrap();
let res = (0..n_groups)
.into_par_iter()
.map(|g| {
if g == reference {
Ok(T::zero())
} else {
let within_g = mean_pairwise_within_impl(groups[g].as_ref(), dist, inner_par);
let between = mean_pairwise_between_impl(
groups[g].as_ref(),
groups[reference].as_ref(),
dist,
inner_par,
)?;
Ok(two * between - within_g - within_ref)
}
})
.collect::<Result<Vec<_>, CoreErrors>>()?;
Ok(res)
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-10;
fn close(a: f64, b: f64) -> bool {
(a - b).abs() < EPS
}
#[test]
fn parse_pert_distance_variants() {
assert!(matches!(
parse_perturbation_distance("euclidean"),
Some(PertDistance::Euclidean)
));
assert!(matches!(
parse_perturbation_distance("Euclidean"),
Some(PertDistance::Euclidean)
));
assert!(matches!(
parse_perturbation_distance("squared_euclidean"),
Some(PertDistance::SquaredEuclidean)
));
assert!(matches!(
parse_perturbation_distance("sqeuclidean"),
Some(PertDistance::SquaredEuclidean)
));
assert!(parse_perturbation_distance("manhattan").is_none());
assert!(parse_perturbation_distance("").is_none());
}
#[test]
fn within_lt_two_rows_is_zero() {
let single = Mat::<f64>::from_fn(1, 3, |_, j| j as f64);
assert_eq!(
mean_pairwise_within(single.as_ref(), PertDistance::Euclidean),
0.0
);
let empty = Mat::<f64>::zeros(0, 3);
assert_eq!(
mean_pairwise_within(empty.as_ref(), PertDistance::Euclidean),
0.0
);
}
#[test]
fn within_three_points_known() {
let m = Mat::<f64>::from_fn(3, 2, |i, j| match (i, j) {
(1, 0) => 3.0,
(2, 1) => 4.0,
_ => 0.0,
});
let d = mean_pairwise_within(m.as_ref(), PertDistance::Euclidean);
assert!(close(d, 4.0));
let d_sq = mean_pairwise_within(m.as_ref(), PertDistance::SquaredEuclidean);
assert!(close(d_sq, (9.0 + 16.0 + 25.0) / 3.0));
}
#[test]
fn between_dim_mismatch_errors() {
let x = Mat::<f64>::zeros(2, 3);
let y = Mat::<f64>::zeros(2, 4);
assert!(matches!(
mean_pairwise_between(x.as_ref(), y.as_ref(), PertDistance::Euclidean),
Err(CoreErrors::NonMatchingFeatureDim { .. })
));
}
#[test]
fn between_empty_returns_zero() {
let x = Mat::<f64>::zeros(0, 2);
let y = Mat::<f64>::from_fn(2, 2, |i, _| i as f64);
assert_eq!(
mean_pairwise_between(x.as_ref(), y.as_ref(), PertDistance::Euclidean).unwrap(),
0.0
);
}
#[test]
fn between_known_value() {
let x = Mat::<f64>::zeros(1, 2);
let y = Mat::<f64>::from_fn(2, 2, |i, j| match (i, j) {
(0, 0) => 3.0,
(0, 1) => 4.0,
(1, 0) => 6.0,
(1, 1) => 8.0,
_ => 0.0,
});
let d = mean_pairwise_between(x.as_ref(), y.as_ref(), PertDistance::Euclidean).unwrap();
assert!(close(d, 7.5));
}
#[test]
fn edistance_symmetric() {
let x = Mat::<f64>::from_fn(4, 3, |i, j| (i + j) as f64);
let y = Mat::<f64>::from_fn(5, 3, |i, j| (i * 2 + j) as f64 - 1.0);
let exy = edistance_two_matrices(x.as_ref(), y.as_ref(), PertDistance::Euclidean).unwrap();
let eyx = edistance_two_matrices(y.as_ref(), x.as_ref(), PertDistance::Euclidean).unwrap();
assert!(close(exy, eyx));
}
#[test]
fn edistance_singletons_squared_identity() {
let x = Mat::<f64>::zeros(1, 2);
let y = Mat::<f64>::from_fn(1, 2, |_, j| if j == 0 { 3.0 } else { 4.0 });
let e =
edistance_two_matrices(x.as_ref(), y.as_ref(), PertDistance::SquaredEuclidean).unwrap();
assert!(close(e, 50.0));
}
#[test]
fn pairwise_edistance_shape_and_symmetry() {
let emb = Mat::<f64>::from_fn(6, 2, |i, j| (i + j) as f64);
let labels = vec![0, 0, 1, 1, 2, 2];
let m = pairwise_edistance(emb.as_ref(), &labels, PertDistance::Euclidean).unwrap();
assert_eq!(m.nrows(), 3);
assert_eq!(m.ncols(), 3);
for i in 0..3 {
assert_eq!(*m.get(i, i), 0.0);
for j in (i + 1)..3 {
assert!(close(*m.get(i, j), *m.get(j, i)));
}
}
}
#[test]
fn pairwise_edistance_label_count_mismatch() {
let emb = Mat::<f64>::zeros(4, 2);
let labels = vec![0_usize, 0, 1];
assert!(matches!(
pairwise_edistance(emb.as_ref(), &labels, PertDistance::Euclidean),
Err(CoreErrors::NumberLabelsNotEqualSampleNumber { .. })
));
}
#[test]
fn onesided_reference_entry_is_zero() {
let emb = Mat::<f64>::from_fn(6, 2, |i, j| (i + j) as f64);
let labels = vec![0_usize, 0, 1, 1, 2, 2];
let res = onesided_edistance(emb.as_ref(), &labels, 1, PertDistance::Euclidean).unwrap();
assert_eq!(res.len(), 3);
assert_eq!(res[1], 0.0);
}
#[test]
fn onesided_matches_pairwise_column() {
let emb = Mat::<f64>::from_fn(8, 3, |i, j| ((i + 1) * (j + 1)) as f64);
let labels = vec![0_usize, 0, 0, 1, 1, 2, 2, 2];
let pw = pairwise_edistance(emb.as_ref(), &labels, PertDistance::Euclidean).unwrap();
let os = onesided_edistance(emb.as_ref(), &labels, 0, PertDistance::Euclidean).unwrap();
for g in 0..3 {
assert!(close(*pw.get(g, 0), os[g]));
}
}
}