#[cfg(all(target_os = "macos", feature = "metal-native"))]
use std::collections::HashMap;
#[cfg(all(target_os = "macos", feature = "metal-native"))]
fn create_test_vectors(count: usize, dimension: usize) -> Vec<hive_gpu::types::GpuVector> {
(0..count)
.map(|i| {
let data: Vec<f32> = (0..dimension)
.map(|d| ((i * dimension + d) as f32) * 0.1)
.collect();
hive_gpu::types::GpuVector::new(format!("vec_{}", i), data)
})
.collect()
}
#[allow(dead_code)]
fn cpu_cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
assert_eq!(a.len(), b.len());
let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
0.0
} else {
dot / (norm_a * norm_b)
}
}
#[allow(dead_code)]
fn cpu_euclidean_distance(a: &[f32], b: &[f32]) -> f32 {
assert_eq!(a.len(), b.len());
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f32>()
.sqrt()
}
#[allow(dead_code)]
fn cpu_dot_product(a: &[f32], b: &[f32]) -> f32 {
assert_eq!(a.len(), b.len());
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
#[cfg(all(target_os = "macos", feature = "metal-native"))]
mod metal_vector_ops_tests {
use super::*;
use hive_gpu::error::HiveGpuError;
use hive_gpu::metal::MetalNativeContext;
use hive_gpu::traits::GpuContext;
use hive_gpu::types::GpuDistanceMetric;
#[test]
fn test_vector_addition_small() {
let context = match MetalNativeContext::new() {
Ok(ctx) => ctx,
Err(HiveGpuError::NoDeviceAvailable) => {
println!("⚠️ Metal not available, skipping test");
return;
}
Err(e) => panic!("Failed to create Metal context: {}", e),
};
let dimension = 10;
let vectors = create_test_vectors(2, dimension);
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Cosine)
.expect("Failed to create storage");
storage
.add_vectors(&vectors)
.expect("Failed to add vectors");
assert_eq!(storage.vector_count(), 2, "Should have 2 vectors");
println!("✅ Small vector addition (10 elements) successful");
println!(" Vectors added: {}", storage.vector_count());
}
#[test]
fn test_vector_addition_medium() {
let context = match MetalNativeContext::new() {
Ok(ctx) => ctx,
Err(HiveGpuError::NoDeviceAvailable) => {
println!("⚠️ Metal not available, skipping test");
return;
}
Err(e) => panic!("Failed to create Metal context: {}", e),
};
let dimension = 1000;
let vectors = create_test_vectors(10, dimension);
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Cosine)
.expect("Failed to create storage");
storage
.add_vectors(&vectors)
.expect("Failed to add vectors");
assert_eq!(storage.vector_count(), 10, "Should have 10 vectors");
println!("✅ Medium vector addition (1000 elements) successful");
println!(" Vectors added: {}", storage.vector_count());
}
#[test]
fn test_vector_addition_large() {
let context = match MetalNativeContext::new() {
Ok(ctx) => ctx,
Err(HiveGpuError::NoDeviceAvailable) => {
println!("⚠️ Metal not available, skipping test");
return;
}
Err(e) => panic!("Failed to create Metal context: {}", e),
};
let dimension = 512; let count = 100; let vectors = create_test_vectors(count, dimension);
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Cosine)
.expect("Failed to create storage");
storage
.add_vectors(&vectors)
.expect("Failed to add vectors");
assert_eq!(
storage.vector_count(),
count,
"Should have {} vectors",
count
);
println!("✅ Large vector addition (100 x 512D) successful");
println!(" Vectors added: {}", storage.vector_count());
}
#[test]
fn test_cosine_similarity_accuracy() {
let context = match MetalNativeContext::new() {
Ok(ctx) => ctx,
Err(HiveGpuError::NoDeviceAvailable) => {
println!("⚠️ Metal not available, skipping test");
return;
}
Err(e) => panic!("Failed to create Metal context: {}", e),
};
let dimension = 128;
let vectors = create_test_vectors(5, dimension);
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Cosine)
.expect("Failed to create storage");
storage
.add_vectors(&vectors)
.expect("Failed to add vectors");
let query = &vectors[0].data;
let results = storage.search(query, 5).expect("Failed to search");
assert!(!results.is_empty(), "Should have search results");
let top_result = &results[0];
println!("✅ Cosine similarity test:");
println!(
" Top result: {} (score: {:.6})",
top_result.id, top_result.score
);
println!(" Expected: vec_0 (score: ~1.0)");
assert!(
(top_result.score - 1.0).abs() < 0.01,
"Self-similarity should be ~1.0, got {}",
top_result.score
);
}
#[test]
fn test_cosine_similarity_orthogonal() {
let context = match MetalNativeContext::new() {
Ok(ctx) => ctx,
Err(HiveGpuError::NoDeviceAvailable) => {
println!("⚠️ Metal not available, skipping test");
return;
}
Err(e) => panic!("Failed to create Metal context: {}", e),
};
let dimension = 4;
let vec1 =
hive_gpu::types::GpuVector::new("orthog_1".to_string(), vec![1.0, 0.0, 0.0, 0.0]);
let vec2 =
hive_gpu::types::GpuVector::new("orthog_2".to_string(), vec![0.0, 1.0, 0.0, 0.0]);
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Cosine)
.expect("Failed to create storage");
storage
.add_vectors(&[vec1, vec2])
.expect("Failed to add vectors");
let query = vec![1.0, 0.0, 0.0, 0.0];
let results = storage.search(&query, 2).expect("Failed to search");
println!("✅ Orthogonal vectors test:");
for result in &results {
println!(" {}: {:.6}", result.id, result.score);
}
assert_eq!(
results[0].id, "orthog_1",
"First result should be self (orthog_1)"
);
assert!(
results[0].score > 0.95,
"Self-similarity should be high, got {}",
results[0].score
);
assert!(
results[1].score < results[0].score,
"Second result should have lower score than first"
);
println!(" ✅ Self-match score: {:.3}", results[0].score);
println!(" ✅ Other vector score: {:.3}", results[1].score);
}
#[test]
fn test_euclidean_distance() {
let context = match MetalNativeContext::new() {
Ok(ctx) => ctx,
Err(HiveGpuError::NoDeviceAvailable) => {
println!("⚠️ Metal not available, skipping test");
return;
}
Err(e) => panic!("Failed to create Metal context: {}", e),
};
let dimension = 3;
let vec1 = hive_gpu::types::GpuVector::new("vec_1".to_string(), vec![0.0, 0.0, 0.0]);
let vec2 = hive_gpu::types::GpuVector::new("vec_2".to_string(), vec![3.0, 4.0, 0.0]);
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Euclidean)
.expect("Failed to create storage");
storage
.add_vectors(&[vec1, vec2])
.expect("Failed to add vectors");
let query = vec![0.0, 0.0, 0.0];
let results = storage.search(&query, 2).expect("Failed to search");
println!("✅ Euclidean distance test:");
for result in &results {
println!(" {}: distance = {:.6}", result.id, result.score);
}
assert_eq!(results[0].id, "vec_1", "Closest should be vec_1");
}
#[test]
fn test_batch_operations() {
let context = match MetalNativeContext::new() {
Ok(ctx) => ctx,
Err(HiveGpuError::NoDeviceAvailable) => {
println!("⚠️ Metal not available, skipping test");
return;
}
Err(e) => panic!("Failed to create Metal context: {}", e),
};
let dimension = 256;
let batch_size = 50;
let vectors = create_test_vectors(batch_size, dimension);
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Cosine)
.expect("Failed to create storage");
let start = std::time::Instant::now();
storage
.add_vectors(&vectors)
.expect("Failed to add vectors");
let duration = start.elapsed();
assert_eq!(
storage.vector_count(),
batch_size,
"Should have {} vectors",
batch_size
);
println!("✅ Batch operations test:");
println!(" Vectors: {}", batch_size);
println!(" Dimension: {}", dimension);
println!(" Time: {:?}", duration);
println!(
" Throughput: {:.2} vectors/sec",
batch_size as f64 / duration.as_secs_f64()
);
}
#[test]
fn test_search_accuracy_k_results() {
let context = match MetalNativeContext::new() {
Ok(ctx) => ctx,
Err(HiveGpuError::NoDeviceAvailable) => {
println!("⚠️ Metal not available, skipping test");
return;
}
Err(e) => panic!("Failed to create Metal context: {}", e),
};
let dimension = 64;
let total_vectors = 20;
let vectors = create_test_vectors(total_vectors, dimension);
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Cosine)
.expect("Failed to create storage");
storage
.add_vectors(&vectors)
.expect("Failed to add vectors");
let query = &vectors[0].data;
let results = storage.search(query, 5).expect("Failed to search");
assert_eq!(results.len(), 5, "Should return exactly 5 results");
let results = storage.search(query, 10).expect("Failed to search");
assert_eq!(results.len(), 10, "Should return exactly 10 results");
let results = storage.search(query, 100).expect("Failed to search");
assert_eq!(
results.len(),
total_vectors,
"Should return all {} available vectors",
total_vectors
);
println!("✅ Search accuracy test (k results):");
println!(" Total vectors: {}", total_vectors);
println!(" k=5: {} results", results.len());
}
#[test]
fn test_edge_case_zero_vector() {
let context = match MetalNativeContext::new() {
Ok(ctx) => ctx,
Err(HiveGpuError::NoDeviceAvailable) => {
println!("⚠️ Metal not available, skipping test");
return;
}
Err(e) => panic!("Failed to create Metal context: {}", e),
};
let dimension = 4;
let zero_vec =
hive_gpu::types::GpuVector::new("zero".to_string(), vec![0.0, 0.0, 0.0, 0.0]);
let normal_vec =
hive_gpu::types::GpuVector::new("normal".to_string(), vec![1.0, 2.0, 3.0, 4.0]);
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Cosine)
.expect("Failed to create storage");
let result = storage.add_vectors(&[zero_vec, normal_vec]);
assert!(result.is_ok(), "Should handle zero vectors gracefully");
println!("✅ Zero vector edge case handled");
}
#[test]
fn test_edge_case_negative_values() {
let context = match MetalNativeContext::new() {
Ok(ctx) => ctx,
Err(HiveGpuError::NoDeviceAvailable) => {
println!("⚠️ Metal not available, skipping test");
return;
}
Err(e) => panic!("Failed to create Metal context: {}", e),
};
let dimension = 4;
let neg_vec =
hive_gpu::types::GpuVector::new("negative".to_string(), vec![-1.0, -2.0, -3.0, -4.0]);
let pos_vec =
hive_gpu::types::GpuVector::new("positive".to_string(), vec![1.0, 2.0, 3.0, 4.0]);
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Cosine)
.expect("Failed to create storage");
storage
.add_vectors(&[neg_vec, pos_vec])
.expect("Failed to add vectors");
let results = storage
.search(&[1.0, 2.0, 3.0, 4.0], 2)
.expect("Failed to search");
println!("✅ Negative values test:");
for result in &results {
println!(" {}: {:.6}", result.id, result.score);
}
let mut scores: HashMap<String, f32> = HashMap::new();
for result in &results {
scores.insert(result.id.clone(), result.score);
}
assert!(
scores.contains_key("positive"),
"Should find positive vector"
);
assert!(
scores.contains_key("negative"),
"Should find negative vector"
);
println!(
" ✅ Positive score: {:.3}",
scores.get("positive").unwrap()
);
println!(
" ✅ Negative score: {:.3}",
scores.get("negative").unwrap()
);
assert_eq!(results.len(), 2, "Should return both vectors");
}
#[test]
fn test_different_distance_metrics() {
let context = match MetalNativeContext::new() {
Ok(ctx) => ctx,
Err(HiveGpuError::NoDeviceAvailable) => {
println!("⚠️ Metal not available, skipping test");
return;
}
Err(e) => panic!("Failed to create Metal context: {}", e),
};
let dimension = 8;
let vectors = create_test_vectors(5, dimension);
println!("✅ Testing distance metrics:");
{
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Cosine)
.expect("Failed to create storage");
storage.add_vectors(&vectors).expect("Failed to add");
let results = storage.search(&vectors[0].data, 3).expect("Failed");
println!(" Cosine: {} results", results.len());
}
{
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::Euclidean)
.expect("Failed to create storage");
storage.add_vectors(&vectors).expect("Failed to add");
let results = storage.search(&vectors[0].data, 3).expect("Failed");
println!(" Euclidean: {} results", results.len());
}
{
let mut storage = context
.create_storage(dimension, GpuDistanceMetric::DotProduct)
.expect("Failed to create storage");
storage.add_vectors(&vectors).expect("Failed to add");
let results = storage.search(&vectors[0].data, 3).expect("Failed");
println!(" DotProduct: {} results", results.len());
}
}
}