1use crate::utils::gemm;
7use faer::Accum;
8use faer::linalg::matmul::triangular::BlockStructure;
9use faer::{Mat, MatRef, Par};
10use rayon::prelude::*;
11
12use crate::prelude::*;
13use crate::utils::faer_parallelism;
14
15const GRAM_ROW_TILE: usize = 512;
21
22#[derive(Debug, Clone, Copy, Default)]
28pub enum PertDistance {
29 #[default]
31 Euclidean,
32 SquaredEuclidean,
37}
38
39pub fn parse_perturbation_distance(s: &str) -> Option<PertDistance> {
49 match s.to_lowercase().as_str() {
50 "euclidean" => Some(PertDistance::Euclidean),
51 "squared_euclidean" | "sqeuclidean" => Some(PertDistance::SquaredEuclidean),
52 _ => None,
53 }
54}
55
56fn row_norms_squared<T>(x: MatRef<T>) -> Vec<T>
73where
74 T: BixverseFloat,
75{
76 let mut out = vec![T::zero(); x.nrows()];
77 out.par_chunks_mut(GRAM_ROW_TILE)
78 .enumerate()
79 .for_each(|(c, chunk)| {
80 let start = c * GRAM_ROW_TILE;
81 for j in 0..x.ncols() {
82 let col = x.col(j).subrows(start, chunk.len());
83 for (o, &v) in chunk.iter_mut().zip(col.iter()) {
84 *o += v * v;
85 }
86 }
87 });
88 out
89}
90
91#[inline(always)]
93fn finish_dist<T: BixverseFloat>(d_sq: T, dist: PertDistance) -> T {
94 match dist {
95 PertDistance::Euclidean => d_sq.sqrt(),
96 PertDistance::SquaredEuclidean => d_sq,
97 }
98}
99
100pub fn mean_pairwise_within<T>(x: MatRef<T>, dist: PertDistance) -> T
113where
114 T: BixverseFloat + std::iter::Sum,
115{
116 mean_pairwise_within_impl(x, dist, true)
117}
118
119fn mean_pairwise_within_impl<T>(x: MatRef<T>, dist: PertDistance, parallel: bool) -> T
131where
132 T: BixverseFloat + std::iter::Sum,
133{
134 let n = x.nrows();
135 if n < 2 {
136 return T::zero();
137 }
138
139 let mut gram = Mat::<T>::zeros(n, n);
141 gemm::gram(
142 gram.as_mut(),
143 BlockStructure::TriangularLower,
144 Accum::Replace,
145 x,
146 x.transpose(),
147 T::one(),
148 if parallel {
149 faer_parallelism()
150 } else {
151 Par::Seq
152 },
153 );
154
155 let norms_sq: Vec<T> = (0..n).map(|i| *gram.get(i, i)).collect();
156 let two = T::from_f64(2.0).unwrap();
157
158 let col_sum = |j: usize| -> T {
160 let mut s = T::zero();
161 for (i, &g) in gram.col(j).iter().enumerate().skip(j + 1) {
162 let d_sq = (norms_sq[i] + norms_sq[j] - two * g).max(T::zero());
164 s += finish_dist(d_sq, dist);
165 }
166 s
167 };
168 let total: T = if parallel {
169 (0..n).into_par_iter().map(col_sum).sum()
170 } else {
171 (0..n).map(col_sum).sum()
172 };
173
174 let n_pairs = T::from_usize(n * (n - 1) / 2).unwrap();
175
176 total / n_pairs
177}
178
179pub fn mean_pairwise_between<T>(
190 x: MatRef<T>,
191 y: MatRef<T>,
192 dist: PertDistance,
193) -> Result<T, CoreErrors>
194where
195 T: BixverseFloat + std::iter::Sum,
196{
197 mean_pairwise_between_impl(x, y, dist, true)
198}
199
200fn mean_pairwise_between_impl<T>(
215 x: MatRef<T>,
216 y: MatRef<T>,
217 dist: PertDistance,
218 parallel: bool,
219) -> Result<T, CoreErrors>
220where
221 T: BixverseFloat + std::iter::Sum,
222{
223 if x.ncols() != y.ncols() {
224 return Err(CoreErrors::NonMatchingFeatureDim {
225 dim_x: x.ncols(),
226 dim_y: y.ncols(),
227 });
228 }
229
230 let n_x = x.nrows();
231 let n_y = y.nrows();
232 if n_x == 0 || n_y == 0 {
234 return Ok(T::zero());
235 }
236
237 let x_norms = row_norms_squared(x);
238 let y_norms = row_norms_squared(y);
239 let two = T::from_f64(2.0).unwrap();
240 let par = if parallel {
241 faer_parallelism()
242 } else {
243 Par::Seq
244 };
245
246 let mut gram = Mat::<T>::zeros(GRAM_ROW_TILE.min(n_x), n_y);
247 let mut total = T::zero();
248 for start in (0..n_x).step_by(GRAM_ROW_TILE) {
249 let rows = GRAM_ROW_TILE.min(n_x - start);
250 let mut tile = gram.as_mut().submatrix_mut(0, 0, rows, n_y);
251 gemm::gemm(
252 tile.as_mut(),
253 Accum::Replace,
254 x.subrows(start, rows),
255 y.transpose(),
256 T::one(),
257 par,
258 );
259 let tile = tile.as_ref();
260 let x_tile = &x_norms[start..start + rows];
261
262 let col_sum = |j: usize| -> T {
263 let mut s = T::zero();
264 for (&g, &xn) in tile.col(j).iter().zip(x_tile) {
265 let d_sq = (xn + y_norms[j] - two * g).max(T::zero());
266 s += finish_dist(d_sq, dist);
267 }
268 s
269 };
270 let tile_total: T = if parallel {
271 (0..n_y).into_par_iter().map(col_sum).sum()
272 } else {
273 (0..n_y).map(col_sum).sum()
274 };
275 total += tile_total;
276 }
277
278 let n_pairs = T::from_usize(n_x * n_y).unwrap();
279
280 Ok(total / n_pairs)
281}
282
283fn group_rows_by_label<T>(embedding: MatRef<T>, labels: &[usize]) -> Result<Vec<Mat<T>>, CoreErrors>
295where
296 T: BixverseFloat,
297{
298 if embedding.nrows() != labels.len() {
299 return Err(CoreErrors::NumberLabelsNotEqualSampleNumber {
300 label_length: labels.len(),
301 n_samples: embedding.nrows(),
302 });
303 }
304
305 let d = embedding.ncols();
306 let n_groups = labels.iter().max().map(|&x| x + 1).unwrap_or(0);
307
308 let mut indices: Vec<Vec<usize>> = vec![Vec::new(); n_groups];
309 for (sample, &g) in labels.iter().enumerate() {
310 indices[g].push(sample);
311 }
312
313 let res = indices
314 .into_par_iter()
315 .map(|rows| Mat::from_fn(rows.len(), d, |i, j| *embedding.get(rows[i], j)))
316 .collect();
317
318 Ok(res)
319}
320
321pub fn edistance_two_matrices<T>(
339 x: MatRef<T>,
340 y: MatRef<T>,
341 dist: PertDistance,
342) -> Result<T, CoreErrors>
343where
344 T: BixverseFloat + std::iter::Sum,
345{
346 let within_x = mean_pairwise_within(x, dist);
347 let within_y = mean_pairwise_within(y, dist);
348 let between = mean_pairwise_between(x, y, dist)?;
349 let two = T::from_f64(2.0).unwrap();
350
351 Ok(two * between - within_x - within_y)
352}
353
354pub(crate) fn edistance_two_matrices_seq<T>(
369 x: MatRef<T>,
370 y: MatRef<T>,
371 dist: PertDistance,
372) -> Result<T, CoreErrors>
373where
374 T: BixverseFloat + std::iter::Sum,
375{
376 let within_x = mean_pairwise_within_impl(x, dist, false);
377 let within_y = mean_pairwise_within_impl(y, dist, false);
378 let between = mean_pairwise_between_impl(x, y, dist, false)?;
379 let two = T::from_f64(2.0).unwrap();
380
381 Ok(two * between - within_x - within_y)
382}
383
384pub fn pairwise_edistance<T>(
396 embedding: MatRef<T>,
397 labels: &[usize],
398 dist: PertDistance,
399) -> Result<Mat<T>, CoreErrors>
400where
401 T: BixverseFloat + std::iter::Sum,
402{
403 let groups = group_rows_by_label(embedding, labels)?;
404 let n_groups = groups.len();
405
406 let inner_par = groups.len() < rayon::current_num_threads();
408 let withins: Vec<T> = groups
409 .par_iter()
410 .map(|g| mean_pairwise_within_impl(g.as_ref(), dist, inner_par))
411 .collect();
412
413 let pairs: Vec<(usize, usize)> = (0..n_groups)
414 .flat_map(|i| (i + 1..n_groups).map(move |j| (i, j)))
415 .collect();
416
417 let two = T::from_f64(2.0).unwrap();
418 let results: Vec<(usize, usize, T)> = pairs
419 .par_iter()
420 .map(|&(i, j)| {
421 let between = mean_pairwise_between_impl(
422 groups[i].as_ref(),
423 groups[j].as_ref(),
424 dist,
425 inner_par,
426 )?;
427 let ed = two * between - withins[i] - withins[j];
428
429 Ok((i, j, ed))
430 })
431 .collect::<Result<Vec<_>, CoreErrors>>()?;
432
433 let mut result = Mat::<T>::zeros(n_groups, n_groups);
434 for (i, j, val) in results {
435 result[(i, j)] = val;
436 result[(j, i)] = val;
437 }
438
439 Ok(result)
440}
441
442pub fn onesided_edistance<T>(
455 embedding: MatRef<T>,
456 labels: &[usize],
457 reference: usize,
458 dist: PertDistance,
459) -> Result<Vec<T>, CoreErrors>
460where
461 T: BixverseFloat + std::iter::Sum,
462{
463 let groups = group_rows_by_label(embedding, labels)?;
464 let n_groups = groups.len();
465 assert!(reference < n_groups, "reference label out of range");
466
467 let inner_par = n_groups < rayon::current_num_threads();
468 let within_ref = mean_pairwise_within(groups[reference].as_ref(), dist);
469 let two = T::from_f64(2.0).unwrap();
470
471 let res = (0..n_groups)
472 .into_par_iter()
473 .map(|g| {
474 if g == reference {
475 Ok(T::zero())
476 } else {
477 let within_g = mean_pairwise_within_impl(groups[g].as_ref(), dist, inner_par);
478 let between = mean_pairwise_between_impl(
479 groups[g].as_ref(),
480 groups[reference].as_ref(),
481 dist,
482 inner_par,
483 )?;
484
485 Ok(two * between - within_g - within_ref)
486 }
487 })
488 .collect::<Result<Vec<_>, CoreErrors>>()?;
489
490 Ok(res)
491}
492
493#[cfg(test)]
498mod tests {
499 use super::*;
500
501 const EPS: f64 = 1e-10;
502
503 fn close(a: f64, b: f64) -> bool {
504 (a - b).abs() < EPS
505 }
506
507 #[test]
509 fn parse_pert_distance_variants() {
510 assert!(matches!(
511 parse_perturbation_distance("euclidean"),
512 Some(PertDistance::Euclidean)
513 ));
514 assert!(matches!(
515 parse_perturbation_distance("Euclidean"),
516 Some(PertDistance::Euclidean)
517 ));
518 assert!(matches!(
519 parse_perturbation_distance("squared_euclidean"),
520 Some(PertDistance::SquaredEuclidean)
521 ));
522 assert!(matches!(
523 parse_perturbation_distance("sqeuclidean"),
524 Some(PertDistance::SquaredEuclidean)
525 ));
526 assert!(parse_perturbation_distance("manhattan").is_none());
527 assert!(parse_perturbation_distance("").is_none());
528 }
529
530 #[test]
532 fn within_lt_two_rows_is_zero() {
533 let single = Mat::<f64>::from_fn(1, 3, |_, j| j as f64);
534 assert_eq!(
535 mean_pairwise_within(single.as_ref(), PertDistance::Euclidean),
536 0.0
537 );
538 let empty = Mat::<f64>::zeros(0, 3);
539 assert_eq!(
540 mean_pairwise_within(empty.as_ref(), PertDistance::Euclidean),
541 0.0
542 );
543 }
544
545 #[test]
547 fn within_three_points_known() {
548 let m = Mat::<f64>::from_fn(3, 2, |i, j| match (i, j) {
550 (1, 0) => 3.0,
551 (2, 1) => 4.0,
552 _ => 0.0,
553 });
554 let d = mean_pairwise_within(m.as_ref(), PertDistance::Euclidean);
555 assert!(close(d, 4.0));
556 let d_sq = mean_pairwise_within(m.as_ref(), PertDistance::SquaredEuclidean);
557 assert!(close(d_sq, (9.0 + 16.0 + 25.0) / 3.0));
558 }
559
560 #[test]
562 fn between_dim_mismatch_errors() {
563 let x = Mat::<f64>::zeros(2, 3);
564 let y = Mat::<f64>::zeros(2, 4);
565 assert!(matches!(
566 mean_pairwise_between(x.as_ref(), y.as_ref(), PertDistance::Euclidean),
567 Err(CoreErrors::NonMatchingFeatureDim { .. })
568 ));
569 }
570
571 #[test]
573 fn between_empty_returns_zero() {
574 let x = Mat::<f64>::zeros(0, 2);
575 let y = Mat::<f64>::from_fn(2, 2, |i, _| i as f64);
576 assert_eq!(
577 mean_pairwise_between(x.as_ref(), y.as_ref(), PertDistance::Euclidean).unwrap(),
578 0.0
579 );
580 }
581
582 #[test]
584 fn between_known_value() {
585 let x = Mat::<f64>::zeros(1, 2);
587 let y = Mat::<f64>::from_fn(2, 2, |i, j| match (i, j) {
588 (0, 0) => 3.0,
589 (0, 1) => 4.0,
590 (1, 0) => 6.0,
591 (1, 1) => 8.0,
592 _ => 0.0,
593 });
594 let d = mean_pairwise_between(x.as_ref(), y.as_ref(), PertDistance::Euclidean).unwrap();
595 assert!(close(d, 7.5));
596 }
597
598 #[test]
600 fn edistance_symmetric() {
601 let x = Mat::<f64>::from_fn(4, 3, |i, j| (i + j) as f64);
602 let y = Mat::<f64>::from_fn(5, 3, |i, j| (i * 2 + j) as f64 - 1.0);
603 let exy = edistance_two_matrices(x.as_ref(), y.as_ref(), PertDistance::Euclidean).unwrap();
604 let eyx = edistance_two_matrices(y.as_ref(), x.as_ref(), PertDistance::Euclidean).unwrap();
605 assert!(close(exy, eyx));
606 }
607
608 #[test]
610 fn edistance_singletons_squared_identity() {
611 let x = Mat::<f64>::zeros(1, 2);
613 let y = Mat::<f64>::from_fn(1, 2, |_, j| if j == 0 { 3.0 } else { 4.0 });
614 let e =
615 edistance_two_matrices(x.as_ref(), y.as_ref(), PertDistance::SquaredEuclidean).unwrap();
616 assert!(close(e, 50.0));
617 }
618
619 #[test]
621 fn pairwise_edistance_shape_and_symmetry() {
622 let emb = Mat::<f64>::from_fn(6, 2, |i, j| (i + j) as f64);
623 let labels = vec![0, 0, 1, 1, 2, 2];
624 let m = pairwise_edistance(emb.as_ref(), &labels, PertDistance::Euclidean).unwrap();
625 assert_eq!(m.nrows(), 3);
626 assert_eq!(m.ncols(), 3);
627 for i in 0..3 {
628 assert_eq!(*m.get(i, i), 0.0);
629 for j in (i + 1)..3 {
630 assert!(close(*m.get(i, j), *m.get(j, i)));
631 }
632 }
633 }
634
635 #[test]
637 fn pairwise_edistance_label_count_mismatch() {
638 let emb = Mat::<f64>::zeros(4, 2);
639 let labels = vec![0_usize, 0, 1];
640 assert!(matches!(
641 pairwise_edistance(emb.as_ref(), &labels, PertDistance::Euclidean),
642 Err(CoreErrors::NumberLabelsNotEqualSampleNumber { .. })
643 ));
644 }
645
646 #[test]
648 fn onesided_reference_entry_is_zero() {
649 let emb = Mat::<f64>::from_fn(6, 2, |i, j| (i + j) as f64);
650 let labels = vec![0_usize, 0, 1, 1, 2, 2];
651 let res = onesided_edistance(emb.as_ref(), &labels, 1, PertDistance::Euclidean).unwrap();
652 assert_eq!(res.len(), 3);
653 assert_eq!(res[1], 0.0);
654 }
655
656 #[test]
658 fn onesided_matches_pairwise_column() {
659 let emb = Mat::<f64>::from_fn(8, 3, |i, j| ((i + 1) * (j + 1)) as f64);
660 let labels = vec![0_usize, 0, 0, 1, 1, 2, 2, 2];
661 let pw = pairwise_edistance(emb.as_ref(), &labels, PertDistance::Euclidean).unwrap();
662 let os = onesided_edistance(emb.as_ref(), &labels, 0, PertDistance::Euclidean).unwrap();
663 for g in 0..3 {
664 assert!(close(*pw.get(g, 0), os[g]));
665 }
666 }
667}