use crate::analysis::finite::ensure_finite_2d;
use crate::errors::AnalysisError;
use ndarray::Array2;
const DEFAULT_T: f32 = 2.0;
pub fn uniformity(embeddings: &Array2<f32>) -> Result<f32, AnalysisError> {
uniformity_with_temperature(embeddings, DEFAULT_T)
}
pub fn uniformity_with_temperature(embeddings: &Array2<f32>, t: f32) -> Result<f32, AnalysisError> {
let n = embeddings.shape()[0];
if n < 2 {
return Err(AnalysisError::InsufficientData(format!(
"Uniformity requires at least 2 embeddings, got {n}"
)));
}
ensure_finite_2d(embeddings, "embeddings for uniformity")?;
let normalized = l2_normalize_rows(embeddings);
let mut exponents = Vec::with_capacity(n * (n - 1) / 2);
for i in 0..n {
let row_i = normalized.row(i);
for j in (i + 1)..n {
let row_j = normalized.row(j);
let sq_dist: f32 = row_i
.iter()
.zip(row_j.iter())
.map(|(a, b)| (a - b).powi(2))
.sum();
exponents.push((-t * sq_dist) as f64);
}
}
if exponents.is_empty() {
return Err(AnalysisError::InsufficientData(
"No valid pairs for uniformity computation".into(),
));
}
let max_exp = exponents.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let sum_shifted: f64 = exponents.iter().map(|&x| (x - max_exp).exp()).sum();
let log_mean = max_exp + (sum_shifted / exponents.len() as f64).ln();
Ok(log_mean as f32)
}
fn l2_normalize_rows(data: &Array2<f32>) -> Array2<f32> {
let mut out = data.to_owned();
for mut row in out.rows_mut() {
let norm = row.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-10);
row.mapv_inplace(|v| v / norm);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::Array2;
#[test]
fn uniformity_of_identical_vectors_is_zero() {
let data = Array2::from_shape_fn((20, 8), |(_i, j)| (j + 1) as f32);
let score = uniformity(&data).unwrap();
approx::assert_relative_eq!(score, 0.0, epsilon = 0.01);
}
#[test]
fn uniformity_of_spread_vectors_is_negative() {
let mut data = Array2::zeros((16, 16));
for i in 0..16 {
data[[i, i]] = 1.0;
}
let score = uniformity(&data).unwrap();
assert!(
score < -1.0,
"Expected strongly negative uniformity, got {score}"
);
}
#[test]
fn uniformity_requires_two_samples() {
let data = Array2::from_elem((1, 4), 1.0_f32);
assert!(uniformity(&data).is_err());
}
#[test]
fn uniformity_rejects_non_finite() {
let mut data = Array2::from_elem((4, 4), 1.0_f32);
data[[2, 3]] = f32::INFINITY;
assert!(uniformity(&data).is_err());
}
#[test]
fn custom_temperature_affects_magnitude() {
let data = Array2::from_shape_fn((10, 8), |(i, j)| ((i * 7 + j * 3) % 11) as f32 / 11.0);
let low_t = uniformity_with_temperature(&data, 1.0).unwrap();
let high_t = uniformity_with_temperature(&data, 4.0).unwrap();
assert!(
high_t < low_t,
"Expected higher t to give more negative uniformity: t=1 => {low_t}, t=4 => {high_t}"
);
}
}