use rayon::prelude::*;
use scirs2_core::ndarray_ext::{Array2, ArrayView2};
use sklears_core::error::{Result as SklResult, SklearsError};
#[inline]
fn to_f32_vec(data: &[f64]) -> Vec<f32> {
data.iter().map(|&x| x as f32).collect()
}
#[inline]
fn to_f64(x: f32) -> f64 {
x as f64
}
pub fn euclidean_distance_f64(a: &[f64], b: &[f64]) -> f64 {
assert_eq!(
a.len(),
b.len(),
"Vectors must have the same length for distance computation"
);
let a_f32 = to_f32_vec(a);
let b_f32 = to_f32_vec(b);
to_f64(sklears_simd::distance::euclidean_distance(&a_f32, &b_f32))
}
pub fn euclidean_distance_squared_f64(a: &[f64], b: &[f64]) -> f64 {
assert_eq!(a.len(), b.len(), "Vectors must have the same length");
let a_f32 = to_f32_vec(a);
let b_f32 = to_f32_vec(b);
let dist = sklears_simd::distance::euclidean_distance(&a_f32, &b_f32);
to_f64(dist * dist)
}
pub fn manhattan_distance_f64(a: &[f64], b: &[f64]) -> f64 {
assert_eq!(a.len(), b.len(), "Vectors must have the same length");
let a_f32 = to_f32_vec(a);
let b_f32 = to_f32_vec(b);
to_f64(sklears_simd::distance::manhattan_distance(&a_f32, &b_f32))
}
pub fn cosine_similarity_f64(a: &[f64], b: &[f64]) -> f64 {
assert_eq!(a.len(), b.len(), "Vectors must have the same length");
let mut dot_product = 0.0;
let mut norm_a = 0.0;
let mut norm_b = 0.0;
for i in 0..a.len() {
dot_product += a[i] * b[i];
norm_a += a[i] * a[i];
norm_b += b[i] * b[i];
}
norm_a = norm_a.sqrt();
norm_b = norm_b.sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
return 0.0;
}
dot_product / (norm_a * norm_b)
}
#[allow(non_snake_case)]
pub fn simd_pairwise_distances(
X: &ArrayView2<f64>,
metric: DistanceMetric,
) -> SklResult<Array2<f64>> {
let (n_samples, _n_features) = X.dim();
let use_parallel = n_samples > 100;
if use_parallel {
simd_pairwise_distances_parallel(X, metric)
} else {
simd_pairwise_distances_serial(X, metric)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DistanceMetric {
Euclidean,
Manhattan,
Cosine,
SquaredEuclidean,
}
#[allow(non_snake_case)]
fn simd_pairwise_distances_serial(
X: &ArrayView2<f64>,
metric: DistanceMetric,
) -> SklResult<Array2<f64>> {
let (n_samples, _n_features) = X.dim();
let mut distances = Array2::<f64>::zeros((n_samples, n_samples));
for i in 0..n_samples {
let row_i = X.row(i).to_vec();
for j in (i + 1)..n_samples {
let row_j = X.row(j).to_vec();
let dist = match metric {
DistanceMetric::Euclidean => euclidean_distance_f64(&row_i, &row_j),
DistanceMetric::Manhattan => manhattan_distance_f64(&row_i, &row_j),
DistanceMetric::Cosine => 1.0 - cosine_similarity_f64(&row_i, &row_j),
DistanceMetric::SquaredEuclidean => euclidean_distance_squared_f64(&row_i, &row_j),
};
distances[[i, j]] = dist;
distances[[j, i]] = dist;
}
}
Ok(distances)
}
#[allow(non_snake_case)]
fn simd_pairwise_distances_parallel(
X: &ArrayView2<f64>,
metric: DistanceMetric,
) -> SklResult<Array2<f64>> {
let (n_samples, _n_features) = X.dim();
let rows: Vec<Vec<f64>> = (0..n_samples)
.into_par_iter()
.map(|i| {
let mut row = vec![0.0; n_samples];
let row_i = X.row(i).to_vec();
#[allow(clippy::needless_range_loop)]
for j in 0..n_samples {
if i == j {
row[j] = 0.0;
} else {
let row_j = X.row(j).to_vec();
row[j] = match metric {
DistanceMetric::Euclidean => euclidean_distance_f64(&row_i, &row_j),
DistanceMetric::Manhattan => manhattan_distance_f64(&row_i, &row_j),
DistanceMetric::Cosine => 1.0 - cosine_similarity_f64(&row_i, &row_j),
DistanceMetric::SquaredEuclidean => {
euclidean_distance_squared_f64(&row_i, &row_j)
}
};
}
}
row
})
.collect();
let mut distances = Array2::<f64>::zeros((n_samples, n_samples));
for (i, row) in rows.into_iter().enumerate() {
for (j, val) in row.into_iter().enumerate() {
distances[[i, j]] = val;
}
}
Ok(distances)
}
#[allow(non_snake_case)]
pub fn simd_knn_graph(
X: &ArrayView2<f64>,
n_neighbors: usize,
sigma: f64,
) -> SklResult<Array2<f64>> {
let (n_samples, _n_features) = X.dim();
if n_neighbors >= n_samples {
return Err(SklearsError::InvalidInput(format!(
"n_neighbors ({}) must be less than n_samples ({})",
n_neighbors, n_samples
)));
}
let adjacency_rows: Vec<Vec<f64>> = (0..n_samples)
.into_par_iter()
.map(|i| {
let row_i = X.row(i).to_vec();
let mut distances: Vec<(usize, f64)> = Vec::with_capacity(n_samples - 1);
for j in 0..n_samples {
if i != j {
let row_j = X.row(j).to_vec();
let dist = euclidean_distance_f64(&row_i, &row_j);
distances.push((j, dist));
}
}
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let mut row = vec![0.0; n_samples];
for &(j, dist) in distances.iter().take(n_neighbors) {
let weight = (-dist * dist / (2.0 * sigma * sigma)).exp();
row[j] = weight;
}
row
})
.collect();
let mut adjacency = Array2::<f64>::zeros((n_samples, n_samples));
for (i, row) in adjacency_rows.into_iter().enumerate() {
for (j, val) in row.into_iter().enumerate() {
adjacency[[i, j]] = val;
}
}
Ok(adjacency)
}
#[derive(Debug, Clone)]
pub struct SimdStats {
pub simd_available: bool,
pub instruction_set: String,
pub expected_speedup: f64,
}
impl SimdStats {
pub fn current() -> Self {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
if is_x86_feature_detected!("avx2") {
return Self {
simd_available: true,
instruction_set: "AVX2".to_string(),
expected_speedup: 6.0,
};
} else if is_x86_feature_detected!("sse2") {
return Self {
simd_available: true,
instruction_set: "SSE2".to_string(),
expected_speedup: 3.0,
};
}
}
Self {
simd_available: false,
instruction_set: "None (scalar fallback)".to_string(),
expected_speedup: 1.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::array;
#[test]
fn test_euclidean_distance_f64() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![4.0, 5.0, 6.0];
let dist = euclidean_distance_f64(&a, &b);
assert!((dist - 5.196152422706632).abs() < 0.01);
}
#[test]
fn test_euclidean_distance_squared_f64() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![4.0, 5.0, 6.0];
let dist_sq = euclidean_distance_squared_f64(&a, &b);
assert!((dist_sq - 27.0).abs() < 0.01);
}
#[test]
fn test_manhattan_distance_f64() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![4.0, 5.0, 6.0];
let dist = manhattan_distance_f64(&a, &b);
assert!((dist - 9.0).abs() < 0.01);
}
#[test]
fn test_cosine_similarity_f64() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![2.0, 4.0, 6.0];
let sim = cosine_similarity_f64(&a, &b);
assert!((sim - 1.0).abs() < 0.01);
}
#[test]
#[allow(non_snake_case)]
fn test_simd_pairwise_distances_euclidean() {
let X = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
let distances = simd_pairwise_distances(&X.view(), DistanceMetric::Euclidean)
.expect("operation should succeed");
assert_eq!(distances.dim(), (4, 4));
for i in 0..4 {
assert_eq!(distances[[i, i]], 0.0);
}
assert!((distances[[0, 1]] - 1.0).abs() < 1e-5);
assert!((distances[[0, 2]] - 1.0).abs() < 1e-5);
assert!((distances[[0, 3]] - 2.0_f64.sqrt()).abs() < 1e-5);
for i in 0..4 {
for j in 0..4 {
assert!((distances[[i, j]] - distances[[j, i]]).abs() < 1e-10);
}
}
}
#[test]
#[allow(non_snake_case)]
fn test_simd_knn_graph() {
let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
let graph = simd_knn_graph(&X.view(), 2, 1.0).expect("operation should succeed");
assert_eq!(graph.dim(), (4, 4));
for i in 0..4 {
assert_eq!(graph[[i, i]], 0.0);
}
for i in 0..4 {
let non_zero = graph.row(i).iter().filter(|&&x| x > 0.0).count();
assert_eq!(non_zero, 2);
}
}
#[test]
fn test_simd_stats() {
let stats = SimdStats::current();
assert!(stats.expected_speedup >= 1.0);
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("sse2") {
assert!(stats.simd_available);
}
}
}
#[test]
#[should_panic(expected = "Vectors must have the same length")]
fn test_euclidean_distance_different_lengths() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![1.0, 2.0];
euclidean_distance_f64(&a, &b);
}
#[test]
#[allow(non_snake_case)]
fn test_simd_pairwise_distances_serial_vs_parallel() {
let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
let serial = simd_pairwise_distances_serial(&X.view(), DistanceMetric::Euclidean)
.expect("operation should succeed");
let parallel = simd_pairwise_distances_parallel(&X.view(), DistanceMetric::Euclidean)
.expect("operation should succeed");
for i in 0..3 {
for j in 0..3 {
assert!((serial[[i, j]] - parallel[[i, j]]).abs() < 1e-10);
}
}
}
#[test]
#[allow(non_snake_case)]
fn test_distance_metrics() {
let X = array![[1.0, 2.0], [4.0, 5.0]];
let euc = simd_pairwise_distances(&X.view(), DistanceMetric::Euclidean)
.expect("operation should succeed");
let man = simd_pairwise_distances(&X.view(), DistanceMetric::Manhattan)
.expect("operation should succeed");
let cos = simd_pairwise_distances(&X.view(), DistanceMetric::Cosine)
.expect("operation should succeed");
let sq_euc = simd_pairwise_distances(&X.view(), DistanceMetric::SquaredEuclidean)
.expect("operation should succeed");
assert_eq!(euc.dim(), (2, 2));
assert_eq!(man.dim(), (2, 2));
assert_eq!(cos.dim(), (2, 2));
assert_eq!(sq_euc.dim(), (2, 2));
assert!((sq_euc[[0, 1]] - euc[[0, 1]].powi(2)).abs() < 1e-5);
}
}