use ahash::AHashMap;
use ndarray::{Array2, ArrayBase, Axis, Data, Ix1, Ix2};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use super::validate_pair;
use crate::math::squared_euclidean_distance_row;
pub use crate::types::DistanceCalculationMetric;
const DEGENERATE_DENOM: f64 = 1e-10;
tunable_gate! {
pub(crate) SILHOUETTE_PARALLEL_MIN_ELEMS => silhouette_parallel_min_elems / set_silhouette_parallel_min_elems = 262_144
}
fn label_index(labels: &[usize]) -> AHashMap<usize, usize> {
let mut index = AHashMap::new();
for &label in labels {
let next = index.len();
index.entry(label).or_insert(next);
}
index
}
fn contingency_matrix(
labels_true: &[usize],
labels_pred: &[usize],
) -> (Array2<usize>, Vec<usize>, Vec<usize>) {
let index_true = label_index(labels_true);
let index_pred = label_index(labels_pred);
let mut matrix = Array2::<usize>::zeros((index_true.len(), index_pred.len()));
for (<, &lp) in labels_true.iter().zip(labels_pred.iter()) {
matrix[[index_true[<], index_pred[&lp]]] += 1;
}
let row_sums = matrix.sum_axis(Axis(1)).to_vec();
let col_sums = matrix.sum_axis(Axis(0)).to_vec();
(matrix, row_sums, col_sums)
}
fn mutual_information(
contingency: &Array2<usize>,
n: usize,
row_sums: &[usize],
col_sums: &[usize],
) -> f64 {
let n_f = n as f64;
let mut mi = 0.0;
for ((i, j), &n_ij) in contingency.indexed_iter() {
if n_ij > 0 {
let n_ij_f = n_ij as f64;
let a = row_sums[i] as f64;
let b = col_sums[j] as f64;
mi += (n_ij_f / n_f) * ((n_f * n_ij_f) / (a * b)).ln();
}
}
mi
}
fn entropy_nats(counts: &[usize], n: usize) -> f64 {
let n_f = n as f64;
let mut h = 0.0;
for &count in counts {
if count > 0 {
let p = count as f64 / n_f;
h -= p * p.ln();
}
}
h
}
fn ln_factorial_table(n_max: usize) -> Vec<f64> {
let mut table = Vec::with_capacity(n_max + 1);
table.push(0.0); let mut acc = 0.0;
for i in 1..=n_max {
acc += (i as f64).ln();
table.push(acc);
}
table
}
fn expected_mutual_information(row_sums: &[usize], col_sums: &[usize], n: usize) -> f64 {
let n_f = n as f64;
let ln_fact = ln_factorial_table(n);
let log_binom = |a: usize, b: usize| ln_fact[a] - ln_fact[b] - ln_fact[a - b];
let mut emi = 0.0;
for &a_i in row_sums {
for &b_j in col_sums {
let lower = (a_i + b_j).saturating_sub(n).max(1);
let upper = a_i.min(b_j);
let log_c_n_bj = log_binom(n, b_j);
for k in lower..=upper {
let log_p = log_binom(a_i, k) + log_binom(n - a_i, b_j - k) - log_c_n_bj;
let term = (k as f64 / n_f) * ((n_f * k as f64) / (a_i as f64 * b_j as f64)).ln();
emi += log_p.exp() * term;
}
}
}
emi
}
fn to_label_vec<S>(labels: &ArrayBase<S, Ix1>) -> Vec<usize>
where
S: Data<Elem = usize>,
{
labels.iter().copied().collect()
}
pub fn normalized_mutual_info<S>(
labels_true: &ArrayBase<S, Ix1>,
labels_pred: &ArrayBase<S, Ix1>,
) -> f64
where
S: Data<Elem = usize>,
{
validate_pair(
labels_true.len(),
labels_pred.len(),
"labels_true and labels_pred",
);
let n = labels_true.len();
let labels_true = to_label_vec(labels_true);
let labels_pred = to_label_vec(labels_pred);
let (contingency, row_sums, col_sums) = contingency_matrix(&labels_true, &labels_pred);
let mi = mutual_information(&contingency, n, &row_sums, &col_sums);
let h_true = entropy_nats(&row_sums, n);
let h_pred = entropy_nats(&col_sums, n);
let denominator = (h_true + h_pred) / 2.0;
if denominator == 0.0 {
0.0
} else {
mi / denominator
}
}
pub fn adjusted_mutual_info<S>(
labels_true: &ArrayBase<S, Ix1>,
labels_pred: &ArrayBase<S, Ix1>,
) -> f64
where
S: Data<Elem = usize>,
{
validate_pair(
labels_true.len(),
labels_pred.len(),
"labels_true and labels_pred",
);
let n = labels_true.len();
let labels_true = to_label_vec(labels_true);
let labels_pred = to_label_vec(labels_pred);
let (contingency, row_sums, col_sums) = contingency_matrix(&labels_true, &labels_pred);
let mi = mutual_information(&contingency, n, &row_sums, &col_sums);
let h_true = entropy_nats(&row_sums, n);
let h_pred = entropy_nats(&col_sums, n);
let emi = expected_mutual_information(&row_sums, &col_sums, n);
let denominator = (h_true + h_pred) / 2.0 - emi;
if denominator.abs() < DEGENERATE_DENOM {
1.0
} else {
(mi - emi) / denominator
}
}
pub fn adjusted_rand_index<S>(
labels_true: &ArrayBase<S, Ix1>,
labels_pred: &ArrayBase<S, Ix1>,
) -> f64
where
S: Data<Elem = usize>,
{
validate_pair(
labels_true.len(),
labels_pred.len(),
"labels_true and labels_pred",
);
let n = labels_true.len();
let labels_true = to_label_vec(labels_true);
let labels_pred = to_label_vec(labels_pred);
let (contingency, row_sums, col_sums) = contingency_matrix(&labels_true, &labels_pred);
let comb2 = |size: usize| {
let size = size as f64;
size * (size - 1.0) / 2.0
};
let sum_comb_cells: f64 = contingency.iter().map(|&n_ij| comb2(n_ij)).sum();
let sum_comb_true: f64 = row_sums.iter().map(|&a| comb2(a)).sum();
let sum_comb_pred: f64 = col_sums.iter().map(|&b| comb2(b)).sum();
let comb_n = comb2(n);
if comb_n == 0.0 {
return 1.0; }
let expected = sum_comb_true * sum_comb_pred / comb_n;
let max_index = 0.5 * (sum_comb_true + sum_comb_pred);
let denominator = max_index - expected;
if denominator.abs() < DEGENERATE_DENOM {
1.0
} else {
(sum_comb_cells - expected) / denominator
}
}
fn accumulate_upper_triangle<S>(
acc: &mut Array2<f64>,
rows: impl Iterator<Item = usize>,
x: &ArrayBase<S, Ix2>,
cluster: &[usize],
metric: DistanceCalculationMetric,
) where
S: Data<Elem = f64>,
{
let n = x.nrows();
for i in rows {
let xi = x.row(i);
for j in (i + 1)..n {
let d = metric.distance(xi, x.row(j));
acc[[i, cluster[j]]] += d;
acc[[j, cluster[i]]] += d;
}
}
}
fn pairwise_cluster_distances<S>(
x: &ArrayBase<S, Ix2>,
cluster: &[usize],
k: usize,
metric: DistanceCalculationMetric,
) -> Array2<f64>
where
S: Data<Elem = f64> + Sync,
{
let n = x.nrows();
let scan_work = n.saturating_mul(n).saturating_mul(x.ncols());
if scan_work < silhouette_parallel_min_elems() {
let mut dist = Array2::<f64>::zeros((n, k));
accumulate_upper_triangle(&mut dist, 0..n, x, cluster, metric);
return dist;
}
let chunks = rayon::current_num_threads().max(1).min(n);
let partials: Vec<Array2<f64>> = (0..chunks)
.into_par_iter()
.map(|c| {
let mut acc = Array2::<f64>::zeros((n, k));
accumulate_upper_triangle(&mut acc, (c..n).step_by(chunks), x, cluster, metric);
acc
})
.collect();
partials
.into_iter()
.reduce(|mut sum, p| {
sum += &p;
sum
})
.unwrap_or_else(|| Array2::<f64>::zeros((n, k)))
}
pub fn silhouette_score<S1, S2>(
x: &ArrayBase<S1, Ix2>,
labels: &ArrayBase<S2, Ix1>,
metric: DistanceCalculationMetric,
) -> f64
where
S1: Data<Elem = f64> + Sync,
S2: Data<Elem = usize>,
{
let n = x.nrows();
let labels = to_label_vec(labels);
let (cluster, k) = validate_clustering_inputs(n, &labels);
let mut sizes = vec![0usize; k];
for &c in &cluster {
sizes[c] += 1;
}
let dist_to_cluster = pairwise_cluster_distances(x, &cluster, k, metric);
let mut total = 0.0;
for i in 0..n {
let own = cluster[i];
if sizes[own] <= 1 {
continue; }
let a = dist_to_cluster[[i, own]] / (sizes[own] - 1) as f64;
let mut b = f64::INFINITY;
for c in 0..k {
if c != own {
let mean_dist = dist_to_cluster[[i, c]] / sizes[c] as f64;
if mean_dist < b {
b = mean_dist;
}
}
}
let denominator = a.max(b);
if denominator > 0.0 {
total += (b - a) / denominator;
}
}
total / n as f64
}
fn validate_clustering_inputs(n_rows: usize, labels: &[usize]) -> (Vec<usize>, usize) {
if n_rows != labels.len() {
panic!(
"dimension mismatch: expected {n_rows}, found {}",
labels.len()
);
}
if n_rows == 0 {
panic!("input is empty: x and labels");
}
let index = label_index(labels);
let k = index.len();
if k < 2 || k >= n_rows {
panic!(
"invalid input: number of clusters is {k}, valid range is 2 to n_samples - 1 ({})",
n_rows - 1
);
}
let cluster = labels.iter().map(|label| index[label]).collect();
(cluster, k)
}
fn cluster_centroids<S>(
x: &ArrayBase<S, Ix2>,
cluster: &[usize],
k: usize,
) -> (Array2<f64>, Vec<usize>)
where
S: Data<Elem = f64>,
{
let mut centroids = Array2::<f64>::zeros((k, x.ncols()));
let mut sizes = vec![0usize; k];
for (i, &c) in cluster.iter().enumerate() {
sizes[c] += 1;
let mut centroid = centroids.row_mut(c);
centroid += &x.row(i);
}
for (mut centroid, &size) in centroids.axis_iter_mut(Axis(0)).zip(sizes.iter()) {
if size > 0 {
centroid /= size as f64;
}
}
(centroids, sizes)
}
fn homogeneity_completeness(labels_true: &[usize], labels_pred: &[usize], n: usize) -> (f64, f64) {
let (contingency, row_sums, col_sums) = contingency_matrix(labels_true, labels_pred);
let mi = mutual_information(&contingency, n, &row_sums, &col_sums);
let h_classes = entropy_nats(&row_sums, n);
let h_clusters = entropy_nats(&col_sums, n);
let homogeneity = if h_classes == 0.0 {
1.0
} else {
mi / h_classes
};
let completeness = if h_clusters == 0.0 {
1.0
} else {
mi / h_clusters
};
(homogeneity, completeness)
}
pub fn homogeneity_score<S>(labels_true: &ArrayBase<S, Ix1>, labels_pred: &ArrayBase<S, Ix1>) -> f64
where
S: Data<Elem = usize>,
{
validate_pair(
labels_true.len(),
labels_pred.len(),
"labels_true and labels_pred",
);
let n = labels_true.len();
homogeneity_completeness(&to_label_vec(labels_true), &to_label_vec(labels_pred), n).0
}
pub fn completeness_score<S>(
labels_true: &ArrayBase<S, Ix1>,
labels_pred: &ArrayBase<S, Ix1>,
) -> f64
where
S: Data<Elem = usize>,
{
validate_pair(
labels_true.len(),
labels_pred.len(),
"labels_true and labels_pred",
);
let n = labels_true.len();
homogeneity_completeness(&to_label_vec(labels_true), &to_label_vec(labels_pred), n).1
}
pub fn v_measure_score<S>(labels_true: &ArrayBase<S, Ix1>, labels_pred: &ArrayBase<S, Ix1>) -> f64
where
S: Data<Elem = usize>,
{
validate_pair(
labels_true.len(),
labels_pred.len(),
"labels_true and labels_pred",
);
let n = labels_true.len();
let (homogeneity, completeness) =
homogeneity_completeness(&to_label_vec(labels_true), &to_label_vec(labels_pred), n);
if homogeneity + completeness == 0.0 {
0.0
} else {
2.0 * homogeneity * completeness / (homogeneity + completeness)
}
}
pub fn fowlkes_mallows_score<S>(
labels_true: &ArrayBase<S, Ix1>,
labels_pred: &ArrayBase<S, Ix1>,
) -> f64
where
S: Data<Elem = usize>,
{
validate_pair(
labels_true.len(),
labels_pred.len(),
"labels_true and labels_pred",
);
let labels_true = to_label_vec(labels_true);
let labels_pred = to_label_vec(labels_pred);
let (contingency, row_sums, col_sums) = contingency_matrix(&labels_true, &labels_pred);
let comb2 = |size: usize| {
let size = size as f64;
size * (size - 1.0) / 2.0
};
let tk: f64 = contingency.iter().map(|&n_ij| comb2(n_ij)).sum(); let pk: f64 = col_sums.iter().map(|&b| comb2(b)).sum(); let qk: f64 = row_sums.iter().map(|&a| comb2(a)).sum();
let denominator = (pk * qk).sqrt();
if denominator == 0.0 {
0.0
} else {
tk / denominator
}
}
pub fn davies_bouldin_score<S1, S2>(x: &ArrayBase<S1, Ix2>, labels: &ArrayBase<S2, Ix1>) -> f64
where
S1: Data<Elem = f64>,
S2: Data<Elem = usize>,
{
let labels = to_label_vec(labels);
let (cluster, k) = validate_clustering_inputs(x.nrows(), &labels);
let (centroids, sizes) = cluster_centroids(x, &cluster, k);
let mut s = vec![0.0_f64; k];
for (i, &c) in cluster.iter().enumerate() {
s[c] += squared_euclidean_distance_row(&x.row(i), ¢roids.row(c)).sqrt();
}
for (distance, &size) in s.iter_mut().zip(sizes.iter()) {
if size > 0 {
*distance /= size as f64;
}
}
let mut db = 0.0;
for i in 0..k {
let mut max_ratio = 0.0_f64;
for j in 0..k {
if i != j {
let centroid_dist =
squared_euclidean_distance_row(¢roids.row(i), ¢roids.row(j)).sqrt();
if centroid_dist > 0.0 {
max_ratio = max_ratio.max((s[i] + s[j]) / centroid_dist);
}
}
}
db += max_ratio;
}
db / k as f64
}
pub fn calinski_harabasz_score<S1, S2>(x: &ArrayBase<S1, Ix2>, labels: &ArrayBase<S2, Ix1>) -> f64
where
S1: Data<Elem = f64>,
S2: Data<Elem = usize>,
{
let n = x.nrows();
let labels = to_label_vec(labels);
let (cluster, k) = validate_clustering_inputs(n, &labels);
let (centroids, sizes) = cluster_centroids(x, &cluster, k);
let overall = x.mean_axis(Axis(0)).unwrap();
let mut between = 0.0;
for (centroid, &size) in centroids.axis_iter(Axis(0)).zip(sizes.iter()) {
between += size as f64 * squared_euclidean_distance_row(¢roid, &overall);
}
let mut within = 0.0;
for (i, &c) in cluster.iter().enumerate() {
within += squared_euclidean_distance_row(&x.row(i), ¢roids.row(c));
}
if within == 0.0 {
return 1.0;
}
(between / within) * ((n - k) as f64 / (k - 1) as f64)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use ndarray::array;
#[test]
fn test_label_index_first_appearance_order() {
let idx = label_index(&[5, 5, 3, 3, 10]);
assert_eq!(idx.len(), 3, "three distinct labels expected");
assert_eq!(idx[&5], 0, "5 appears first, should map to 0");
assert_eq!(idx[&3], 1, "3 appears second, should map to 1");
assert_eq!(idx[&10], 2, "10 appears third, should map to 2");
}
#[test]
fn test_label_index_single_label() {
let idx = label_index(&[7, 7, 7]);
assert_eq!(idx.len(), 1);
assert_eq!(idx[&7], 0);
}
#[test]
fn test_label_index_all_distinct() {
let idx = label_index(&[10, 20, 30]);
assert_eq!(idx.len(), 3);
assert_eq!(idx[&10], 0);
assert_eq!(idx[&20], 1);
assert_eq!(idx[&30], 2);
}
#[test]
fn test_contingency_matrix_uniform() {
let (mat, row_sums, col_sums) = contingency_matrix(&[0, 0, 1, 1], &[0, 1, 0, 1]);
assert_eq!(mat.shape(), &[2, 2]);
assert_eq!(mat[[0, 0]], 1);
assert_eq!(mat[[0, 1]], 1);
assert_eq!(mat[[1, 0]], 1);
assert_eq!(mat[[1, 1]], 1);
assert_eq!(row_sums, vec![2, 2]);
assert_eq!(col_sums, vec![2, 2]);
}
#[test]
fn test_contingency_matrix_identical_labels() {
let (mat, row_sums, col_sums) = contingency_matrix(&[0, 0, 1, 1], &[0, 0, 1, 1]);
assert_eq!(mat.shape(), &[2, 2]);
assert_eq!(mat[[0, 0]], 2, "class-0 samples all in pred-cluster-0");
assert_eq!(mat[[0, 1]], 0);
assert_eq!(mat[[1, 0]], 0);
assert_eq!(mat[[1, 1]], 2, "class-1 samples all in pred-cluster-1");
assert_eq!(row_sums, vec![2, 2]);
assert_eq!(col_sums, vec![2, 2]);
}
#[test]
fn test_entropy_nats_two_equal_clusters() {
let h = entropy_nats(&[2, 2], 4);
assert_abs_diff_eq!(h, std::f64::consts::LN_2, epsilon = 1e-10);
}
#[test]
fn test_entropy_nats_single_cluster() {
let h = entropy_nats(&[4], 4);
assert_abs_diff_eq!(h, 0.0, epsilon = 1e-10);
}
#[test]
fn test_entropy_nats_four_equal_clusters() {
let h = entropy_nats(&[1, 1, 1, 1], 4);
let expected = (4.0_f64).ln(); assert_abs_diff_eq!(h, expected, epsilon = 1e-10);
}
#[test]
fn test_entropy_nats_zero_count_skipped() {
let h = entropy_nats(&[0, 4], 4);
assert_abs_diff_eq!(h, 0.0, epsilon = 1e-10);
}
#[test]
fn test_mutual_information_independent() {
let mat = array![[1usize, 1], [1, 1]];
let mi = mutual_information(&mat, 4, &[2, 2], &[2, 2]);
assert_abs_diff_eq!(mi, 0.0, epsilon = 1e-10);
}
#[test]
fn test_mutual_information_identical() {
let mat = array![[2usize, 0], [0, 2]];
let mi = mutual_information(&mat, 4, &[2, 2], &[2, 2]);
assert_abs_diff_eq!(mi, std::f64::consts::LN_2, epsilon = 1e-10);
}
#[test]
fn test_homogeneity_completeness_pure_clusters() {
let labels_true = [0usize, 0, 1, 1];
let labels_pred = [0usize, 1, 2, 3];
let (h, c) = homogeneity_completeness(&labels_true, &labels_pred, 4);
assert_abs_diff_eq!(h, 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(c, 0.5, epsilon = 1e-10);
}
#[test]
fn test_homogeneity_completeness_identical() {
let labels = [0usize, 0, 1, 1];
let (h, c) = homogeneity_completeness(&labels, &labels, 4);
assert_abs_diff_eq!(h, 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(c, 1.0, epsilon = 1e-10);
}
#[test]
fn test_homogeneity_completeness_swapped_roles() {
let labels_true = [0usize, 1, 2, 3];
let labels_pred = [0usize, 0, 1, 1];
let (h, c) = homogeneity_completeness(&labels_true, &labels_pred, 4);
assert_abs_diff_eq!(h, 0.5, epsilon = 1e-10);
assert_abs_diff_eq!(c, 1.0, epsilon = 1e-10);
}
#[test]
fn test_entropy_nats_unequal_clusters() {
let h = entropy_nats(&[1, 3], 4);
let expected =
-(1.0_f64 / 4.0) * (1.0_f64 / 4.0).ln() - (3.0_f64 / 4.0) * (3.0_f64 / 4.0).ln();
assert_abs_diff_eq!(h, expected, epsilon = 1e-10);
}
#[test]
fn test_mutual_information_pure_clusters() {
let mat = array![[1usize, 1, 0, 0], [0, 0, 1, 1]];
let mi = mutual_information(&mat, 4, &[2, 2], &[1, 1, 1, 1]);
assert_abs_diff_eq!(mi, std::f64::consts::LN_2, epsilon = 1e-10);
}
#[test]
fn test_expected_mutual_information_symmetric_2x2() {
let emi = expected_mutual_information(&[2, 2], &[2, 2], 4);
let expected = std::f64::consts::LN_2 / 3.0;
assert_abs_diff_eq!(emi, expected, epsilon = 1e-12);
}
#[test]
fn test_ln_factorial_table_known_values() {
let table = ln_factorial_table(5);
assert_eq!(table.len(), 6, "length should be n_max + 1");
assert_abs_diff_eq!(table[0], 0.0, epsilon = 1e-12); assert_abs_diff_eq!(table[1], 0.0, epsilon = 1e-12); assert_abs_diff_eq!(table[5], (120.0_f64).ln(), epsilon = 1e-10); }
#[test]
fn test_cluster_centroids_known_means_and_sizes() {
let x = array![[0.0, 0.0], [2.0, 0.0], [10.0, 10.0]];
let cluster = [0usize, 0, 1];
let (centroids, sizes) = cluster_centroids(&x, &cluster, 2);
assert_eq!(centroids.shape(), &[2, 2]);
assert_abs_diff_eq!(centroids[[0, 0]], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(centroids[[0, 1]], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(centroids[[1, 0]], 10.0, epsilon = 1e-12);
assert_abs_diff_eq!(centroids[[1, 1]], 10.0, epsilon = 1e-12);
assert_eq!(sizes, vec![2, 1]);
}
fn pseudo_random_matrix(rows: usize, cols: usize, seed: u64) -> Array2<f64> {
Array2::from_shape_fn((rows, cols), |(i, j)| {
let t = (seed as f64) * 0.731 + (i * cols + j) as f64 * 0.618_033_988_7;
(t.sin() * 43758.5453).fract() - 0.5
})
}
fn brute_force_dist_to_cluster(
x: &Array2<f64>,
cluster: &[usize],
k: usize,
metric: DistanceCalculationMetric,
) -> Array2<f64> {
let n = x.nrows();
let mut dist = Array2::<f64>::zeros((n, k));
for i in 0..n {
for j in 0..n {
dist[[i, cluster[j]]] += metric.distance(x.row(i), x.row(j));
}
}
dist
}
#[test]
fn test_pairwise_cluster_distances_serial_matches_full_scan_bitwise() {
let x = pseudo_random_matrix(12, 5, 1); let cluster: Vec<usize> = (0..12).map(|i| i % 3).collect();
let got = pairwise_cluster_distances(&x, &cluster, 3, DistanceCalculationMetric::Euclidean);
let want =
brute_force_dist_to_cluster(&x, &cluster, 3, DistanceCalculationMetric::Euclidean);
assert!(
got.iter()
.zip(want.iter())
.all(|(a, b)| a.to_bits() == b.to_bits()),
"serial symmetric fill must be bitwise identical to the full scan"
);
}
#[test]
fn test_pairwise_cluster_distances_parallel_matches_full_scan() {
let n = 300; let x = pseudo_random_matrix(n, 4, 2);
let cluster: Vec<usize> = (0..n).map(|i| i % 7).collect();
for metric in [
DistanceCalculationMetric::Euclidean,
DistanceCalculationMetric::Manhattan,
DistanceCalculationMetric::Minkowski(3.0),
] {
let got = pairwise_cluster_distances(&x, &cluster, 7, metric);
let want = brute_force_dist_to_cluster(&x, &cluster, 7, metric);
assert_eq!(got.shape(), want.shape());
for (a, b) in got.iter().zip(want.iter()) {
assert!(
(a - b).abs() <= 1e-9,
"parallel fill {a} deviates from full-scan reference {b} for {metric:?}"
);
}
}
}
#[test]
fn test_pairwise_cluster_distances_parallel_run_to_run_deterministic() {
let n = 300;
let x = pseudo_random_matrix(n, 4, 3);
let cluster: Vec<usize> = (0..n).map(|i| i % 5).collect();
let a = pairwise_cluster_distances(&x, &cluster, 5, DistanceCalculationMetric::Euclidean);
let b = pairwise_cluster_distances(&x, &cluster, 5, DistanceCalculationMetric::Euclidean);
assert!(
a.iter()
.zip(b.iter())
.all(|(x, y)| x.to_bits() == y.to_bits()),
"repeated parallel fills must be bitwise identical"
);
}
}