use ann_search_rs::utils::dist::Dist;
use rayon::prelude::*;
use crate::data::structures::CompressedSparseData;
use crate::errors::ManifoldsError;
use crate::utils::sparse_ops::*;
use crate::utils::traits::*;
pub fn apply_log_potential<T>(
matrix: &CompressedSparseData<T>,
epsilon: T,
) -> CompressedSparseData<T>
where
T: ManifoldsFloat,
{
assert!(matrix.cs_type.is_csr(), "Matrix must be CSR format");
let data: Vec<T> = matrix
.data
.par_iter()
.map(|&val| {
let clamped = val.min(T::one()).max(T::zero());
-(clamped + epsilon).ln()
})
.collect();
CompressedSparseData::new_csr(&data, &matrix.indices, &matrix.indptr, matrix.shape())
}
pub fn apply_power_potential<T>(
matrix: &CompressedSparseData<T>,
gamma: T,
) -> CompressedSparseData<T>
where
T: ManifoldsFloat,
{
assert!(matrix.cs_type.is_csr(), "Matrix must be CSR format");
if (gamma + T::one()).abs() < T::from(1e-10).unwrap() {
return matrix.clone();
}
let c = (T::one() - gamma) / T::from(2.0).unwrap();
let data: Vec<T> = matrix.data.par_iter().map(|&val| val.powf(c) / c).collect();
CompressedSparseData::new_csr(&data, &matrix.indices, &matrix.indptr, matrix.shape())
}
pub fn calculate_potential<T>(
diffusion_op: &CompressedSparseData<T>,
t: usize,
gamma: T,
) -> Result<CompressedSparseData<T>, ManifoldsError>
where
T: ManifoldsFloat,
{
let diffused = matrix_power(diffusion_op, t)?;
if (gamma - T::one()).abs() < T::from(1e-10).unwrap() {
Ok(apply_log_potential(&diffused, T::from(1e-7).unwrap()))
} else if (gamma + T::one()).abs() < T::from(1e-10).unwrap() {
Ok(diffused)
} else {
Ok(apply_power_potential(&diffused, gamma))
}
}
fn csr_row_to_dense<T>(matrix: &CompressedSparseData<T>, row_idx: usize) -> Vec<T>
where
T: ManifoldsFloat,
{
let ncols = matrix.shape().1;
let mut dense = vec![T::zero(); ncols];
let start = matrix.indptr[row_idx];
let end = matrix.indptr[row_idx + 1];
for idx in start..end {
let col = matrix.indices[idx];
dense[col] = matrix.data[idx];
}
dense
}
pub fn compute_potential_distances<T>(
potential: &CompressedSparseData<T>,
metric: &Dist,
) -> Vec<Vec<T>>
where
T: ManifoldsFloat,
{
assert!(potential.cs_type.is_csr(), "Matrix must be CSR format");
let n = potential.shape().0;
let dense_rows: Vec<Vec<T>> = (0..n)
.into_par_iter()
.map(|i| csr_row_to_dense(potential, i))
.collect();
let norms: Vec<T> = if matches!(metric, Dist::Cosine) {
dense_rows
.par_iter()
.map(|row| T::calculate_l2_norm(row))
.collect()
} else {
Vec::new()
};
let pairs: Vec<(usize, usize)> = (0..n)
.flat_map(|i| (i + 1..n).map(move |j| (i, j)))
.collect();
let computed: Vec<T> = pairs
.par_iter()
.map(|&(i, j)| match metric {
Dist::SquaredEuclidean => T::euclidean_simd(&dense_rows[i], &dense_rows[j]).sqrt(),
Dist::Cosine => {
let dot = T::dot_simd(&dense_rows[i], &dense_rows[j]);
let denom = norms[i] * norms[j];
if denom > T::zero() {
T::one() - (dot / denom)
} else {
T::zero()
}
}
Dist::Manhattan => unreachable!(),
})
.collect();
let mut dist = vec![vec![T::zero(); n]; n];
for ((i, j), d) in pairs.iter().zip(computed.iter()) {
dist[*i][*j] = *d;
dist[*j][*i] = *d;
}
dist
}
#[cfg(test)]
mod test_potential_transforms {
use super::*;
use approx::assert_relative_eq;
use num_traits::Float;
fn create_test_matrix() -> CompressedSparseData<f64> {
let data = vec![0.5, 0.3, 0.2, 0.7, 0.3, 0.8, 0.2];
let indices = vec![0, 1, 2, 0, 2, 1, 2];
let indptr = vec![0, 3, 5, 7];
CompressedSparseData::new_csr(&data, &indices, &indptr, (3, 3))
}
#[test]
fn test_log_potential_values() {
let mat = create_test_matrix();
let result = apply_log_potential(&mat, 1e-7);
assert_eq!(result.indices, mat.indices);
assert_eq!(result.indptr, mat.indptr);
assert_eq!(result.data.len(), mat.data.len());
let epsilon = 1e-7;
assert_relative_eq!(result.data[0], -(0.5 + epsilon).ln(), epsilon = 1e-10);
assert_relative_eq!(result.data[1], -(0.3 + epsilon).ln(), epsilon = 1e-10);
assert_relative_eq!(result.data[2], -(0.2 + epsilon).ln(), epsilon = 1e-10);
for &val in &result.data {
assert!(val > 0.0);
}
}
#[test]
fn test_log_potential_epsilon_prevents_inf() {
let data = vec![1e-10, 1e-20, 0.5];
let indices = vec![0, 1, 2];
let indptr = vec![0, 2, 3];
let mat = CompressedSparseData::new_csr(&data, &indices, &indptr, (2, 3));
let result = apply_log_potential(&mat, 1e-7);
for &val in &result.data {
assert!(val.is_finite());
}
}
#[test]
fn test_power_potential_gamma_negative_one() {
let mat = create_test_matrix();
let result = apply_power_potential(&mat, -1.0);
assert_eq!(result.data, mat.data);
assert_eq!(result.indices, mat.indices);
assert_eq!(result.indptr, mat.indptr);
}
#[test]
fn test_power_potential_values() {
let mat = create_test_matrix();
let gamma = 0.5;
let result = apply_power_potential(&mat, gamma);
let c = (1.0 - gamma) / 2.0;
assert_eq!(result.indices, mat.indices);
assert_eq!(result.indptr, mat.indptr);
assert_relative_eq!(result.data[0], 0.5_f64.powf(c) / c, epsilon = 1e-10);
assert_relative_eq!(result.data[1], 0.3_f64.powf(c) / c, epsilon = 1e-10);
assert_relative_eq!(result.data[2], 0.2_f64.powf(c) / c, epsilon = 1e-10);
}
#[test]
fn test_power_potential_gamma_zero() {
let mat = create_test_matrix();
let gamma = 0.0;
let result = apply_power_potential(&mat, gamma);
let c = 0.5;
assert_relative_eq!(result.data[0], 0.5_f64.powf(c) / c, epsilon = 1e-10);
}
#[test]
fn test_calculate_potential_log() {
let data = vec![0.5, 0.5, 0.4, 0.6, 1.0];
let indices = vec![0, 1, 0, 1, 2];
let indptr = vec![0, 2, 4, 5];
let diff_op = CompressedSparseData::new_csr(&data, &indices, &indptr, (3, 3));
let result = calculate_potential(&diff_op, 2, 1.0).unwrap();
assert!(!result.data.is_empty());
for &val in &result.data {
assert!(val.is_finite());
}
let positive_count = result.data.iter().filter(|&&v| v > 0.0).count();
assert!(positive_count > 0, "Expected some positive potentials");
}
#[test]
fn test_calculate_potential_identity() {
let data = vec![0.5, 0.5, 0.4, 0.6, 1.0];
let indices = vec![0, 1, 0, 1, 2];
let indptr = vec![0, 2, 4, 5];
let diff_op = CompressedSparseData::new_csr(&data, &indices, &indptr, (3, 3));
let result = calculate_potential(&diff_op, 2, -1.0).unwrap();
let p2 = matrix_power(&diff_op, 2).unwrap();
assert_eq!(result.data.len(), p2.data.len());
for (res_val, p2_val) in result.data.iter().zip(&p2.data) {
assert_relative_eq!(res_val, p2_val, epsilon = 1e-10);
}
}
#[test]
fn test_calculate_potential_power() {
let data = vec![0.5, 0.5, 0.4, 0.6, 1.0];
let indices = vec![0, 1, 0, 1, 2];
let indptr = vec![0, 2, 4, 5];
let diff_op = CompressedSparseData::new_csr(&data, &indices, &indptr, (3, 3));
let result = calculate_potential(&diff_op, 2, 0.5).unwrap();
assert!(!result.data.is_empty());
for &val in &result.data {
assert!(val.is_finite());
assert!(val > 0.0);
}
}
#[test]
fn test_potential_preserves_sparsity() {
let mat = create_test_matrix();
let nnz = mat.data.len();
let log_result = apply_log_potential(&mat, 1e-7);
assert_eq!(log_result.data.len(), nnz);
let power_result = apply_power_potential(&mat, 0.5);
assert_eq!(power_result.data.len(), nnz);
}
}
#[cfg(test)]
mod test_distance_computation {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_csr_row_to_dense() {
let data = vec![0.5, 0.3, 0.7];
let indices = vec![0, 2, 1];
let indptr = vec![0, 2, 3];
let mat = CompressedSparseData::new_csr(&data, &indices, &indptr, (2, 3));
let row0 = csr_row_to_dense(&mat, 0);
assert_eq!(row0, vec![0.5, 0.0, 0.3]);
let row1 = csr_row_to_dense(&mat, 1);
assert_eq!(row1, vec![0.0, 0.7, 0.0]);
}
#[test]
fn test_euclidean_distances() {
let data = vec![1.0, 1.0, 1.0];
let indices = vec![0, 1, 2];
let indptr = vec![0, 1, 2, 3];
let mat = CompressedSparseData::new_csr(&data, &indices, &indptr, (3, 3));
let distances = compute_potential_distances(&mat, &Dist::SquaredEuclidean);
for i in 0..3 {
assert_relative_eq!(distances[i][i], 0.0, epsilon = 1e-10);
}
assert_relative_eq!(distances[0][1], 2.0_f64.sqrt(), epsilon = 1e-10);
assert_relative_eq!(distances[1][0], 2.0_f64.sqrt(), epsilon = 1e-10);
assert_relative_eq!(distances[0][2], 2.0_f64.sqrt(), epsilon = 1e-10);
}
#[test]
fn test_cosine_distances() {
let data = vec![1.0, 1.0, 1.0, 1.0];
let indices = vec![0, 1, 0, 1];
let indptr = vec![0, 1, 2, 4];
let mat = CompressedSparseData::new_csr(&data, &indices, &indptr, (3, 3));
let distances = compute_potential_distances(&mat, &Dist::Cosine);
assert_relative_eq!(distances[0][1], 1.0, epsilon = 1e-10);
let expected = 1.0 - (1.0 / 2.0_f64.sqrt());
assert_relative_eq!(distances[0][2], expected, epsilon = 1e-10);
}
#[test]
fn test_symmetry() {
let data = vec![0.5, 0.3, 0.7, 0.2, 0.4, 0.6];
let indices = vec![0, 1, 1, 2, 0, 2];
let indptr = vec![0, 2, 4, 6];
let mat = CompressedSparseData::new_csr(&data, &indices, &indptr, (3, 3));
let distances = compute_potential_distances(&mat, &Dist::SquaredEuclidean);
for i in 0..3 {
for j in 0..3 {
assert_relative_eq!(distances[i][j], distances[j][i], epsilon = 1e-10);
}
}
}
}