ruvector/crates/ruvector-postgres/tests/unit_halfvec_tests.rs

312 lines
9.6 KiB
Rust

//! Unit tests for HalfVec (half-precision f16) type
//!
//! Tests half-precision vector storage and conversions
use ruvector_postgres::types::HalfVec;
use half::f16;
#[cfg(test)]
mod halfvec_tests {
use super::*;
// ========================================================================
// Construction Tests
// ========================================================================
#[test]
fn test_from_f32_basic() {
let data = [1.0, 2.0, 3.0];
let hv = HalfVec::from_f32(&data);
assert_eq!(hv.dimensions(), 3);
}
#[test]
fn test_from_f32_precision_loss() {
// f16 has less precision than f32
let original = [1.23456789, 9.87654321];
let hv = HalfVec::from_f32(&original);
let recovered = hv.to_f32();
// Should be close but not exact due to f16 precision
for (orig, rec) in original.iter().zip(recovered.iter()) {
assert!((orig - rec).abs() < 0.01);
}
}
#[test]
fn test_from_f32_empty() {
let data: [f32; 0] = [];
let hv = HalfVec::from_f32(&data);
assert_eq!(hv.dimensions(), 0);
}
#[test]
fn test_from_f32_large() {
let size = 1000;
let data: Vec<f32> = (0..size).map(|i| i as f32 * 0.1).collect();
let hv = HalfVec::from_f32(&data);
assert_eq!(hv.dimensions(), size);
}
// ========================================================================
// Conversion Tests
// ========================================================================
#[test]
fn test_f32_roundtrip_simple() {
let original = [1.0, 2.0, 3.0, 4.0, 5.0];
let hv = HalfVec::from_f32(&original);
let recovered = hv.to_f32();
assert_eq!(recovered.len(), 5);
for (orig, rec) in original.iter().zip(recovered.iter()) {
assert!((orig - rec).abs() < 0.001);
}
}
#[test]
fn test_f32_roundtrip_negative() {
let original = [-1.5, 2.3, -4.7, 0.0, -0.001];
let hv = HalfVec::from_f32(&original);
let recovered = hv.to_f32();
for (orig, rec) in original.iter().zip(recovered.iter()) {
assert!((orig - rec).abs() < 0.01);
}
}
#[test]
fn test_f32_roundtrip_extreme_values() {
// Test values near f16 limits
let original = [0.00001, 100.0, -100.0, 0.5];
let hv = HalfVec::from_f32(&original);
let recovered = hv.to_f32();
for (orig, rec) in original.iter().zip(recovered.iter()) {
// Relative error for extreme values
let rel_error = if orig.abs() > 0.0 {
((orig - rec) / orig).abs()
} else {
(orig - rec).abs()
};
assert!(rel_error < 0.01 || (orig - rec).abs() < 0.01);
}
}
// ========================================================================
// Memory Efficiency Tests
// ========================================================================
#[test]
fn test_memory_size() {
let data: Vec<f32> = (0..100).map(|i| i as f32).collect();
let hv = HalfVec::from_f32(&data);
// HalfVec should use ~50% of the memory of RuVector
// Data portion: 100 elements * 2 bytes = 200 bytes
// Plus header (4 bytes for dims/padding)
let data_size = hv.data_memory_size();
assert!(data_size >= 200 && data_size <= 210);
}
#[test]
fn test_memory_savings() {
use ruvector_postgres::types::RuVector;
let size = 1000;
let data: Vec<f32> = (0..size).map(|i| i as f32).collect();
let rv = RuVector::from_slice(&data);
let hv = HalfVec::from_f32(&data);
let rv_size = rv.data_memory_size();
let hv_size = hv.data_memory_size();
// HalfVec should be approximately half the size
// (Header is the same size, so not exactly half)
let ratio = hv_size as f64 / rv_size as f64;
assert!(ratio < 0.55 && ratio > 0.45);
}
// ========================================================================
// Accuracy Tests
// ========================================================================
#[test]
fn test_integer_values_exact() {
// Small integers should be represented exactly in f16
let integers = [0.0, 1.0, 2.0, 3.0, 10.0, 100.0, -50.0];
let hv = HalfVec::from_f32(&integers);
let recovered = hv.to_f32();
for (orig, rec) in integers.iter().zip(recovered.iter()) {
if orig.abs() < 1000.0 {
assert_eq!(*orig, rec, "Integer {} should be exact", orig);
}
}
}
#[test]
fn test_zero_preservation() {
let zeros = [0.0, -0.0, 0.0, -0.0];
let hv = HalfVec::from_f32(&zeros);
let recovered = hv.to_f32();
for rec in recovered.iter() {
assert_eq!(*rec, 0.0);
}
}
#[test]
fn test_sign_preservation() {
let values = [1.0, -1.0, 2.5, -2.5, 0.1, -0.1];
let hv = HalfVec::from_f32(&values);
let recovered = hv.to_f32();
for (orig, rec) in values.iter().zip(recovered.iter()) {
assert_eq!(orig.signum(), rec.signum(),
"Sign should be preserved for {}", orig);
}
}
// ========================================================================
// Edge Cases
// ========================================================================
#[test]
fn test_single_element() {
let data = [42.0];
let hv = HalfVec::from_f32(&data);
assert_eq!(hv.dimensions(), 1);
let recovered = hv.to_f32();
assert_eq!(recovered.len(), 1);
assert!((recovered[0] - 42.0).abs() < 0.1);
}
#[test]
fn test_power_of_two_sizes() {
// Test sizes that align with SIMD boundaries
for size in [8, 16, 32, 64, 128, 256, 512, 1024] {
let data: Vec<f32> = (0..size).map(|i| i as f32 * 0.1).collect();
let hv = HalfVec::from_f32(&data);
assert_eq!(hv.dimensions(), size);
let recovered = hv.to_f32();
assert_eq!(recovered.len(), size);
}
}
#[test]
fn test_non_power_of_two_sizes() {
// Test sizes that don't align with SIMD boundaries
for size in [7, 15, 31, 63, 127, 255] {
let data: Vec<f32> = (0..size).map(|i| i as f32 * 0.1).collect();
let hv = HalfVec::from_f32(&data);
assert_eq!(hv.dimensions(), size);
}
}
// ========================================================================
// Numerical Range Tests
// ========================================================================
#[test]
fn test_small_values() {
// Test values near f16's minimum normal value
let small = [0.0001, 0.001, 0.01, 0.1];
let hv = HalfVec::from_f32(&small);
let recovered = hv.to_f32();
for (orig, rec) in small.iter().zip(recovered.iter()) {
assert!((orig - rec).abs() < 0.001 || (orig - rec) / orig < 0.1);
}
}
#[test]
fn test_large_values() {
// Test values approaching f16's maximum
let large = [100.0, 500.0, 1000.0];
let hv = HalfVec::from_f32(&large);
let recovered = hv.to_f32();
for (orig, rec) in large.iter().zip(recovered.iter()) {
let rel_error = ((orig - rec) / orig).abs();
assert!(rel_error < 0.01, "Large value {} -> {}, error {}", orig, rec, rel_error);
}
}
#[test]
fn test_mixed_magnitude() {
// Test vectors with widely varying magnitudes
let mixed = [0.001, 1.0, 100.0, 0.01, 10.0];
let hv = HalfVec::from_f32(&mixed);
let recovered = hv.to_f32();
for (orig, rec) in mixed.iter().zip(recovered.iter()) {
let abs_error = (orig - rec).abs();
let rel_error = if orig.abs() > 0.0 {
abs_error / orig.abs()
} else {
abs_error
};
assert!(rel_error < 0.05 || abs_error < 0.01);
}
}
// ========================================================================
// Clone and Equality Tests
// ========================================================================
#[test]
fn test_clone() {
let data = [1.0, 2.0, 3.0];
let hv1 = HalfVec::from_f32(&data);
let hv2 = hv1; // Copy (since HalfVec is Copy)
assert_eq!(hv1.dimensions(), hv2.dimensions());
assert_eq!(hv1.to_f32(), hv2.to_f32());
}
// ========================================================================
// Stress Tests
// ========================================================================
#[test]
fn test_large_batch_conversion() {
let num_vectors = 1000;
let dim = 128;
for i in 0..num_vectors {
let data: Vec<f32> = (0..dim)
.map(|j| ((i * dim + j) as f32) * 0.001)
.collect();
let hv = HalfVec::from_f32(&data);
assert_eq!(hv.dimensions(), dim);
let recovered = hv.to_f32();
assert_eq!(recovered.len(), dim);
}
}
#[test]
fn test_alternating_pattern() {
let size = 100;
let data: Vec<f32> = (0..size)
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
.collect();
let hv = HalfVec::from_f32(&data);
let recovered = hv.to_f32();
for (i, rec) in recovered.iter().enumerate() {
let expected = if i % 2 == 0 { 1.0 } else { -1.0 };
assert_eq!(*rec, expected);
}
}
}