use std::io::Cursor;
use rand::prelude::*;
use crate::brute_force::{BruteForceRabitqIndex, BruteForceSearchParams};
use crate::index::RabitqIndex;
use crate::io::{
read_fvecs_from_reader, read_groundtruth_from_reader, read_ids_from_reader,
read_ivecs_from_reader,
};
use crate::ivf::{IvfRabitqIndex, SearchParams};
use crate::kmeans::run_kmeans;
use crate::quantizer::{quantize_with_centroid, reconstruct_into, RabitqConfig};
use crate::{Metric, RabitqError, RoaringBitmap, RotatorType};
fn random_vector(dim: usize, rng: &mut StdRng) -> Vec<f32> {
(0..dim).map(|_| rng.gen::<f32>() * 2.0 - 1.0).collect()
}
mod float_comparison {
pub fn approx_eq(a: f32, b: f32, rel_tol: f32, abs_tol: f32) -> bool {
let diff = (a - b).abs();
if diff <= abs_tol {
return true;
}
let max_val = a.abs().max(b.abs());
if max_val < 1e-6 {
return diff < abs_tol * 10.0; }
diff / max_val <= rel_tol
}
pub fn score_acceptable(
fastscan_score: f32,
naive_score: f32,
rel_tol: f32,
abs_tol: f32,
) -> bool {
approx_eq(fastscan_score, naive_score, rel_tol, abs_tol)
}
pub fn score_acceptable_1bit(fs: f32, nv: f32) -> bool {
score_acceptable(fs, nv, 0.05, 0.2) }
pub fn score_acceptable_3bit(fs: f32, nv: f32) -> bool {
score_acceptable(fs, nv, 0.08, 0.3) }
pub fn score_acceptable_7bit(fs: f32, nv: f32) -> bool {
score_acceptable(fs, nv, 0.03, 0.15) }
}
#[test]
fn quantizer_reconstruction_is_reasonable() {
let dim = 64;
let mut rng = StdRng::seed_from_u64(1234);
let data = random_vector(dim, &mut rng);
let centroid = vec![0.0f32; dim];
let config = RabitqConfig::new(7);
let quantized = quantize_with_centroid(&data, ¢roid, &config, Metric::L2);
assert_eq!(
quantized.unpack_ex_code().len(),
dim,
"extended code length mismatch"
);
assert_eq!(
quantized.unpack_binary_code().len(),
dim,
"binary code length mismatch"
);
let mut reconstructed = vec![0.0f32; dim];
reconstruct_into(¢roid, &quantized, &mut reconstructed);
let mut diff_sqr = 0.0f32;
let mut norm_sqr = 0.0f32;
for (a, b) in data.iter().zip(reconstructed.iter()) {
let diff = a - b;
diff_sqr += diff * diff;
norm_sqr += a * a;
}
let rel_error = if norm_sqr > f32::EPSILON {
(diff_sqr.sqrt()) / norm_sqr.sqrt()
} else {
0.0
};
assert!(
rel_error < 0.3,
"relative reconstruction error too high: {rel_error}"
);
}
#[test]
fn ivf_search_recovers_identical_vectors() {
let dim = 32;
let total = 256;
let mut rng = StdRng::seed_from_u64(4321);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
32,
7,
Metric::L2,
RotatorType::FhtKacRotator,
7777,
false,
)
.expect("train index");
let params = SearchParams::new(20, 32);
for (idx, vector) in data.iter().take(16).enumerate() {
let results = index.search(vector, params).expect("search");
assert!(!results.is_empty(), "no results returned for query {idx}");
let target_result = results.iter().find(|r| r.id == idx);
if target_result.is_none() {
println!(
"Query {} - target not in top-{} results:",
idx, params.top_k
);
println!(" (This may occur due to quantization error or SIMD precision differences)");
for (i, r) in results.iter().take(10).enumerate() {
println!(" [{}] ID={}, score={:.6}", i, r.id, r.score);
}
}
assert!(
target_result.is_some(),
"failed to find vector {idx} in top-{} results",
params.top_k
);
let target_distance = target_result.unwrap().score;
let max_distance = 150.0; assert!(
target_distance < max_distance,
"query vector {idx} distance too large: {target_distance} (threshold: {max_distance})"
);
}
}
#[test]
fn fastscan_matches_naive_l2() {
let dim = 48;
let total = 320;
let mut rng = StdRng::seed_from_u64(2468);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
40,
1, Metric::L2,
RotatorType::FhtKacRotator,
1357,
false,
)
.expect("train index");
let params = SearchParams::new(5, 12);
let mut total_evals = 0usize;
for (query_idx, query) in data.iter().take(12).enumerate() {
let (fastscan, diag) = index
.search_with_diagnostics(query, params)
.expect("fastscan search");
total_evals += diag.extended_evaluations;
let naive = index.search_naive(query, params).expect("naive search");
assert_eq!(
fastscan.len(),
naive.len(),
"Result count mismatch for query {}",
query_idx
);
for (i, nv) in naive.iter().enumerate() {
let fs_match = fastscan.iter().find(|fs| fs.id == nv.id);
if let Some(fs) = fs_match {
let acceptable = float_comparison::score_acceptable_1bit(fs.score, nv.score);
assert!(
acceptable,
"Query {}: Score mismatch for ID {}: fastscan={:.6}, naive={:.6}, diff={:.6}",
query_idx,
nv.id,
fs.score,
nv.score,
(fs.score - nv.score).abs()
);
} else {
if i >= params.top_k - 2 {
println!(
"Query {}: ID {} in naive pos {} not in FastScan top-{} (boundary effect)",
query_idx, nv.id, i, params.top_k
);
} else {
panic!(
"Query {}: ID {} missing from FastScan results at position {}",
query_idx, nv.id, i
);
}
}
}
}
let _ = total_evals; }
#[test]
fn fastscan_matches_naive_ip() {
let dim = 24;
let total = 200;
let mut rng = StdRng::seed_from_u64(9753);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
25,
1, Metric::InnerProduct,
RotatorType::FhtKacRotator,
8642,
false,
)
.expect("train index");
let params = SearchParams::new(7, 10);
let mut total_evals = 0usize;
for (query_idx, query) in data.iter().take(12).enumerate() {
let (fastscan, diag) = index
.search_with_diagnostics(query, params)
.expect("fastscan search");
total_evals += diag.extended_evaluations;
let naive = index.search_naive(query, params).expect("naive search");
assert_eq!(
fastscan.len(),
naive.len(),
"Result count mismatch for query {}",
query_idx
);
for (i, nv) in naive.iter().enumerate() {
let fs_match = fastscan.iter().find(|fs| fs.id == nv.id);
if let Some(fs) = fs_match {
let acceptable = float_comparison::score_acceptable_1bit(fs.score, nv.score);
assert!(
acceptable,
"Query {}: Score mismatch for ID {}: fastscan={:.6}, naive={:.6}, diff={:.6}",
query_idx,
nv.id,
fs.score,
nv.score,
(fs.score - nv.score).abs()
);
} else {
if i >= params.top_k - 2 {
println!(
"Query {}: ID {} in naive pos {} not in FastScan top-{} (boundary effect)",
query_idx, nv.id, i, params.top_k
);
} else {
panic!(
"Query {}: ID {} missing from FastScan results at position {}",
query_idx, nv.id, i
);
}
}
}
}
let _ = total_evals; }
#[test]
fn one_bit_search_has_no_extended_pruning() {
let dim = 20;
let total = 120;
let mut rng = StdRng::seed_from_u64(4242);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
16,
1,
Metric::L2,
RotatorType::FhtKacRotator,
2024,
false,
)
.expect("train index");
let params = SearchParams::new(4, 10);
for query in data.iter().take(6) {
let (fastscan, diag) = index
.search_with_diagnostics(query, params)
.expect("fastscan search");
let naive = index.search_naive(query, params).expect("naive search");
assert_eq!(fastscan.len(), naive.len(), "result count mismatch");
for (fs, nv) in fastscan.iter().zip(naive.iter()) {
let score_acceptable = float_comparison::score_acceptable_1bit(fs.score, nv.score);
assert!(
score_acceptable,
"Score mismatch: fastscan ID={} score={:.6}, naive ID={} score={:.6}",
fs.id, fs.score, nv.id, nv.score
);
}
assert_eq!(
diag.extended_evaluations, 0,
"extended path should not be evaluated when ex_bits=0"
);
assert!(
diag.estimated > 0,
"no candidates were fully estimated in one-bit search"
);
}
}
#[test]
fn index_persistence_roundtrip() {
let dim = 24;
let total = 200;
let mut rng = StdRng::seed_from_u64(7412);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
32,
7,
Metric::InnerProduct,
RotatorType::FhtKacRotator,
9999,
false,
)
.expect("train index");
let mut buffer = Vec::new();
index.save_to_writer(&mut buffer).expect("serialize index");
let restored = IvfRabitqIndex::load_from_reader(buffer.as_slice()).expect("deserialize index");
assert_eq!(
restored.len(),
index.len(),
"vector count changed after reload"
);
let params = SearchParams::new(5, 12);
for query in data.iter().take(5) {
let original = index.search(query, params).expect("search original");
let roundtrip = restored.search(query, params).expect("search restored");
assert_eq!(original, roundtrip, "round-tripped results diverged");
}
}
#[test]
fn index_persistence_detects_corruption() {
let dim = 16;
let total = 128;
let mut rng = StdRng::seed_from_u64(0xFACE);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
24,
7, Metric::L2,
RotatorType::FhtKacRotator,
4242,
false,
)
.expect("train index");
let mut buffer = Vec::new();
index.save_to_writer(&mut buffer).expect("serialize index");
let checksum_offset = buffer.len().saturating_sub(4);
assert!(checksum_offset > 0, "buffer too small for checksum test");
buffer[checksum_offset - 1] ^= 0xAA;
match IvfRabitqIndex::load_from_reader(buffer.as_slice()) {
Err(RabitqError::InvalidPersistence(_msg)) => {
}
other => panic!("expected InvalidPersistence error, got {other:?}"),
}
}
#[test]
fn index_persistence_validates_vector_count() {
let dim = 12;
let total = 96;
let mut rng = StdRng::seed_from_u64(0xC0FFEE);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
16,
3, Metric::InnerProduct,
RotatorType::FhtKacRotator,
9001,
false,
)
.expect("train index");
let mut buffer = Vec::new();
index.save_to_writer(&mut buffer).expect("serialize index");
let vector_count_offset = 4 + 4 + 4 + 4 + 1 + 1 + 1 + 1; let checksum_offset = buffer.len().saturating_sub(4);
assert!(checksum_offset > vector_count_offset + 8);
let mut raw = [0u8; 8];
raw.copy_from_slice(&buffer[vector_count_offset..vector_count_offset + 8]);
let original = u64::from_le_bytes(raw);
let updated = original + 1;
buffer[vector_count_offset..vector_count_offset + 8].copy_from_slice(&updated.to_le_bytes());
let mut hasher = crc32fast::Hasher::new();
hasher.update(&buffer[8..checksum_offset]);
let new_checksum = hasher.finalize();
buffer[checksum_offset..].copy_from_slice(&new_checksum.to_le_bytes());
match IvfRabitqIndex::load_from_reader(buffer.as_slice()) {
Err(RabitqError::InvalidPersistence(msg)) => {
assert_eq!(
msg, "vector count metadata mismatch",
"unexpected error message"
);
}
other => panic!("expected vector count mismatch, got {other:?}"),
}
}
#[test]
fn fvecs_reader_parses_vectors() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&(3i32).to_le_bytes());
bytes.extend_from_slice(&1.0f32.to_le_bytes());
bytes.extend_from_slice(&2.0f32.to_le_bytes());
bytes.extend_from_slice(&3.0f32.to_le_bytes());
bytes.extend_from_slice(&(3i32).to_le_bytes());
bytes.extend_from_slice(&4.0f32.to_le_bytes());
bytes.extend_from_slice(&5.0f32.to_le_bytes());
bytes.extend_from_slice(&6.0f32.to_le_bytes());
let vectors = read_fvecs_from_reader(Cursor::new(bytes.clone()), None).expect("read fvecs");
assert_eq!(vectors.len(), 2);
assert_eq!(vectors[0], vec![1.0, 2.0, 3.0]);
assert_eq!(vectors[1], vec![4.0, 5.0, 6.0]);
let limited = read_fvecs_from_reader(Cursor::new(bytes), Some(1)).expect("read fvecs limit");
assert_eq!(limited.len(), 1);
}
#[test]
fn ivecs_reader_parses_vectors() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&(2i32).to_le_bytes());
bytes.extend_from_slice(&7i32.to_le_bytes());
bytes.extend_from_slice(&8i32.to_le_bytes());
bytes.extend_from_slice(&(2i32).to_le_bytes());
bytes.extend_from_slice(&1i32.to_le_bytes());
bytes.extend_from_slice(&9i32.to_le_bytes());
let rows = read_ivecs_from_reader(Cursor::new(bytes), None).expect("read ivecs");
assert_eq!(rows, vec![vec![7, 8], vec![1, 9]]);
let mut id_bytes = Vec::new();
id_bytes.extend_from_slice(&(1i32).to_le_bytes());
id_bytes.extend_from_slice(&5i32.to_le_bytes());
id_bytes.extend_from_slice(&(1i32).to_le_bytes());
id_bytes.extend_from_slice(&6i32.to_le_bytes());
id_bytes.extend_from_slice(&(1i32).to_le_bytes());
id_bytes.extend_from_slice(&11i32.to_le_bytes());
let ids = read_ids_from_reader(Cursor::new(id_bytes), None).expect("read ids");
assert_eq!(ids, vec![5usize, 6usize, 11usize]);
let mut gt_bytes = Vec::new();
gt_bytes.extend_from_slice(&(3i32).to_le_bytes());
gt_bytes.extend_from_slice(&0i32.to_le_bytes());
gt_bytes.extend_from_slice(&1i32.to_le_bytes());
gt_bytes.extend_from_slice(&2i32.to_le_bytes());
gt_bytes.extend_from_slice(&(2i32).to_le_bytes());
gt_bytes.extend_from_slice(&3i32.to_le_bytes());
gt_bytes.extend_from_slice(&4i32.to_le_bytes());
let groundtruth =
read_groundtruth_from_reader(Cursor::new(gt_bytes), None).expect("read groundtruth");
assert_eq!(groundtruth, vec![vec![0, 1, 2], vec![3, 4]]);
}
#[test]
fn third_party_kmeans_converges_on_separated_clusters() {
let data = vec![
vec![0.0, 0.0],
vec![0.2, -0.1],
vec![-0.1, 0.3],
vec![4.0, 4.2],
vec![3.9, 4.1],
vec![4.3, 3.8],
];
let mut rng = StdRng::seed_from_u64(0xACED);
let result = run_kmeans(&data, 2, 25, &mut rng);
assert_eq!(result.centroids.len(), 2);
assert_eq!(result.assignments.len(), data.len());
let mut centroids = result.centroids.clone();
centroids.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
let left = ¢roids[0];
let right = ¢roids[1];
assert!(
left[0].abs() < 0.5 && left[1].abs() < 0.5,
"left centroid: {:?}",
left
);
assert!(
(right[0] - 4.0).abs() < 0.5 && (right[1] - 4.0).abs() < 0.5,
"right centroid: {:?}",
right
);
let first_cluster = result.assignments[0];
assert!(result.assignments[1..3]
.iter()
.all(|&assignment| assignment == first_cluster));
let second_cluster = result.assignments[3];
assert!(result.assignments[4..]
.iter()
.all(|&assignment| assignment == second_cluster));
assert_ne!(first_cluster, second_cluster);
}
#[test]
fn preclustered_training_matches_naive_l2() {
let dim = 28;
let total = 240;
let mut rng = StdRng::seed_from_u64(0xA51CE);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let mut kmeans_rng = StdRng::seed_from_u64(0x5EED);
let kmeans = run_kmeans(&data, 32, 25, &mut kmeans_rng);
let index = IvfRabitqIndex::train_with_clusters(
&data,
&kmeans.centroids,
&kmeans.assignments,
7,
Metric::L2,
RotatorType::FhtKacRotator,
0xBEEF,
false,
)
.expect("train with clusters");
let params = SearchParams::new(6, 16);
for query in data.iter().take(8) {
let (fastscan, _) = index
.search_with_diagnostics(query, params)
.expect("fastscan search");
let naive = index.search_naive(query, params).expect("naive search");
assert_eq!(fastscan.len(), naive.len(), "Result count mismatch");
for (i, (fs, nv)) in fastscan.iter().zip(naive.iter()).enumerate() {
let score_diff = (fs.score - nv.score).abs();
let acceptable = if nv.score.abs() < 0.1 {
score_diff < 0.1
} else {
let rel_error = score_diff / nv.score.abs();
rel_error < 0.01 || score_diff < 1e-5
};
if fs.id != nv.id && !acceptable {
println!("ID mismatch at position {}: fastscan id={} score={:.6}, naive id={} score={:.6}, diff={:.6}",
i, fs.id, fs.score, nv.id, nv.score, score_diff);
}
assert!(
acceptable,
"Score mismatch at position {} (id fs={}, nv={}): fastscan={:.6}, naive={:.6}, diff={:.6}",
i, fs.id, nv.id, fs.score, nv.score, score_diff
);
}
}
}
#[test]
fn preclustered_training_matches_naive_ip() {
let dim = 18;
let total = 180;
let mut rng = StdRng::seed_from_u64(0xDEADBEEF);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let mut kmeans_rng = StdRng::seed_from_u64(0xB16B00B5);
let kmeans = run_kmeans(&data, 24, 30, &mut kmeans_rng);
let index = IvfRabitqIndex::train_with_clusters(
&data,
&kmeans.centroids,
&kmeans.assignments,
7,
Metric::InnerProduct,
RotatorType::FhtKacRotator,
0x1234_5678,
false,
)
.expect("train with clusters");
let params = SearchParams::new(5, 12);
for query in data.iter().take(10) {
let (fastscan, _) = index
.search_with_diagnostics(query, params)
.expect("fastscan search");
let naive = index.search_naive(query, params).expect("naive search");
assert_eq!(fastscan.len(), naive.len(), "Result count mismatch");
for (i, (fs, nv)) in fastscan.iter().zip(naive.iter()).enumerate() {
let score_diff = (fs.score - nv.score).abs();
let acceptable = if nv.score.abs() < 0.1 {
score_diff < 0.1
} else {
let rel_error = score_diff / nv.score.abs();
rel_error < 0.02 || score_diff < 1e-5
};
if fs.id != nv.id && !acceptable {
println!("ID mismatch at position {}: fastscan id={} score={:.6}, naive id={} score={:.6}, diff={:.6}",
i, fs.id, fs.score, nv.id, nv.score, score_diff);
}
assert!(
acceptable,
"Score mismatch at position {} (id fs={}, nv={}): fastscan={:.6}, naive={:.6}, diff={:.6}",
i, fs.id, nv.id, fs.score, nv.score, score_diff
);
}
}
}
#[test]
fn filtered_search_respects_bitmap() {
let dim = 32;
let total = 100;
let mut rng = StdRng::seed_from_u64(9999);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
16,
7,
Metric::L2,
RotatorType::FhtKacRotator,
1111,
false,
)
.expect("train index");
let mut filter = RoaringBitmap::new();
for id in 10..20 {
filter.insert(id);
}
let params = SearchParams::new(5, 16);
let query = &data[0];
let filtered_results = index
.search_filtered(query, params, &filter)
.expect("filtered search");
for result in &filtered_results {
assert!(
filter.contains(result.id as u32),
"result id {} not in filter",
result.id
);
}
}
#[test]
fn filtered_search_returns_correct_results() {
let dim = 32;
let total = 200;
let mut rng = StdRng::seed_from_u64(8888);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
20,
7,
Metric::L2,
RotatorType::FhtKacRotator,
2222,
false,
)
.expect("train index");
let mut filter = RoaringBitmap::new();
for id in (0..total).step_by(2) {
filter.insert(id as u32);
}
let params = SearchParams::new(10, 20);
let query = &data[50];
let filtered_results = index
.search_filtered(query, params, &filter)
.expect("filtered search");
let unfiltered_results = index.search(query, params).expect("unfiltered search");
for result in &filtered_results {
assert!(
filter.contains(result.id as u32),
"filtered result id {} not in filter",
result.id
);
}
for result in &filtered_results {
assert_eq!(
result.id % 2,
0,
"filtered result id {} is odd but should be even",
result.id
);
}
let unfiltered_filtered_ids: Vec<usize> = unfiltered_results
.iter()
.filter(|r| filter.contains(r.id as u32))
.map(|r| r.id)
.collect();
let filtered_ids: Vec<usize> = filtered_results.iter().map(|r| r.id).collect();
let min_len = filtered_ids.len().min(unfiltered_filtered_ids.len());
assert_eq!(
&filtered_ids[..min_len],
&unfiltered_filtered_ids[..min_len],
"filtered search results differ from manually filtered unfiltered results"
);
}
#[test]
fn filtered_search_with_empty_filter() {
let dim = 32;
let total = 50;
let mut rng = StdRng::seed_from_u64(7777);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
10,
7,
Metric::L2,
RotatorType::FhtKacRotator,
3333,
false,
)
.expect("train index");
let filter = RoaringBitmap::new();
let params = SearchParams::new(5, 10);
let query = &data[0];
let results = index
.search_filtered(query, params, &filter)
.expect("filtered search");
assert!(
results.is_empty(),
"expected empty results with empty filter, got {}",
results.len()
);
}
#[test]
fn brute_force_search_recovers_identical_vectors() {
let dim = 32;
let total = 256;
let mut rng = StdRng::seed_from_u64(5555);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = BruteForceRabitqIndex::train(
&data,
7,
Metric::L2,
RotatorType::FhtKacRotator,
8888,
false,
)
.expect("train brute-force index");
let params = BruteForceSearchParams::new(1);
for (idx, vector) in data.iter().take(16).enumerate() {
let results = index.search(vector, params).expect("search");
assert!(!results.is_empty(), "no results returned for query {idx}");
assert_eq!(
results[0].id, idx,
"failed to retrieve vector {idx} from brute-force index"
);
}
}
#[test]
fn brute_force_l2_search_is_consistent() {
let dim = 48;
let total = 200;
let mut rng = StdRng::seed_from_u64(6666);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = BruteForceRabitqIndex::train(
&data,
7,
Metric::L2,
RotatorType::FhtKacRotator,
9999,
false,
)
.expect("train brute-force index");
let params = BruteForceSearchParams::new(10);
let query = random_vector(dim, &mut rng);
let results = index.search(&query, params).expect("search");
assert_eq!(results.len(), 10, "expected 10 results");
for i in 1..results.len() {
assert!(
results[i].score >= results[i - 1].score,
"results not sorted by distance"
);
}
}
#[test]
fn brute_force_inner_product_search_is_consistent() {
let dim = 48;
let total = 200;
let mut rng = StdRng::seed_from_u64(7777);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = BruteForceRabitqIndex::train(
&data,
7,
Metric::InnerProduct,
RotatorType::FhtKacRotator,
11111,
false,
)
.expect("train brute-force index");
let params = BruteForceSearchParams::new(10);
let query = random_vector(dim, &mut rng);
let results = index.search(&query, params).expect("search");
assert_eq!(results.len(), 10, "expected 10 results");
for i in 1..results.len() {
assert!(
results[i].score <= results[i - 1].score,
"results not sorted by score (descending)"
);
}
}
#[test]
fn brute_force_persistence_roundtrip() {
let dim = 32;
let total = 100;
let mut rng = StdRng::seed_from_u64(8888);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = BruteForceRabitqIndex::train(
&data,
7,
Metric::L2,
RotatorType::FhtKacRotator,
12345,
false,
)
.expect("train brute-force index");
let mut buffer = Vec::new();
index
.save_to_writer(&mut buffer)
.expect("save brute-force index");
let loaded = BruteForceRabitqIndex::load_from_reader(Cursor::new(&buffer))
.expect("load brute-force index");
assert_eq!(loaded.len(), index.len(), "loaded index has wrong length");
let params = BruteForceSearchParams::new(5);
let query = &data[0];
let results_original = index.search(query, params).expect("search original");
let results_loaded = loaded.search(query, params).expect("search loaded");
assert_eq!(
results_original.len(),
results_loaded.len(),
"result count mismatch"
);
for (orig, load) in results_original.iter().zip(results_loaded.iter()) {
assert_eq!(orig.id, load.id, "id mismatch after roundtrip");
assert!(
(orig.score - load.score).abs() < 1e-5,
"score mismatch after roundtrip"
);
}
}
#[test]
fn brute_force_filtered_search_works() {
let dim = 32;
let total = 100;
let mut rng = StdRng::seed_from_u64(9999);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = BruteForceRabitqIndex::train(
&data,
7,
Metric::L2,
RotatorType::FhtKacRotator,
22222,
false,
)
.expect("train brute-force index");
let mut filter = RoaringBitmap::new();
for i in (10..30).step_by(2) {
filter.insert(i);
}
let params = BruteForceSearchParams::new(5);
let query = &data[0];
let results = index
.search_filtered(query, params, &filter)
.expect("filtered search");
for result in &results {
assert!(
filter.contains(result.id as u32),
"result id {} not in filter",
result.id
);
}
assert!(results.len() <= 5, "too many results returned");
}
#[test]
fn brute_force_faster_config_works() {
let dim = 64;
let total = 150;
let mut rng = StdRng::seed_from_u64(10000);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = BruteForceRabitqIndex::train(
&data,
7,
Metric::L2,
RotatorType::FhtKacRotator,
33333,
true, )
.expect("train brute-force index with faster_config");
let params = BruteForceSearchParams::new(10);
let query = &data[0];
let results = index
.search(query, params)
.expect("search with faster_config");
assert!(!results.is_empty(), "no results with faster_config");
assert_eq!(
results[0].id, 0,
"failed to find self with faster_config enabled"
);
}
#[test]
fn smart_loader_detects_ivf_index() {
let dim = 32;
let total = 100;
let mut rng = StdRng::seed_from_u64(44444);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let ivf_index = IvfRabitqIndex::train(
&data,
16,
7,
Metric::L2,
RotatorType::FhtKacRotator,
55555,
false,
)
.expect("train IVF index");
let mut buffer = Vec::new();
ivf_index
.save_to_writer(&mut buffer)
.expect("save IVF index");
let loaded = RabitqIndex::load_from_reader(Cursor::new(&buffer)).expect("smart load IVF index");
assert!(loaded.is_ivf(), "should detect IVF index");
assert!(!loaded.is_brute_force(), "should not be brute force");
assert_eq!(loaded.len(), total, "loaded index has wrong length");
let unwrapped = loaded.as_ivf().expect("should be IVF");
assert_eq!(unwrapped.cluster_count(), 16, "wrong cluster count");
}
#[test]
fn smart_loader_detects_brute_force_index() {
let dim = 32;
let total = 100;
let mut rng = StdRng::seed_from_u64(66666);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let bf_index = BruteForceRabitqIndex::train(
&data,
7,
Metric::L2,
RotatorType::FhtKacRotator,
77777,
false,
)
.expect("train BruteForce index");
let mut buffer = Vec::new();
bf_index
.save_to_writer(&mut buffer)
.expect("save BruteForce index");
let loaded =
RabitqIndex::load_from_reader(Cursor::new(&buffer)).expect("smart load BruteForce index");
assert!(loaded.is_brute_force(), "should detect BruteForce index");
assert!(!loaded.is_ivf(), "should not be IVF");
assert_eq!(loaded.len(), total, "loaded index has wrong length");
let unwrapped = loaded.as_brute_force().expect("should be BruteForce");
assert_eq!(unwrapped.len(), total, "wrong vector count");
}
#[test]
fn smart_loader_rejects_invalid_magic() {
let mut buffer = vec![b'X', b'X', b'X', b'X'];
buffer.extend_from_slice(&[0u8; 100]);
let result = RabitqIndex::load_from_reader(Cursor::new(&buffer));
assert!(result.is_err(), "should reject invalid magic");
match result {
Err(RabitqError::InvalidPersistence(msg)) => {
assert!(
msg.contains("unrecognized") || msg.contains("magic"),
"error message should mention magic header"
);
}
_ => panic!("expected InvalidPersistence error"),
}
}
#[test]
fn smart_loader_search_works_correctly() {
let dim = 32;
let total = 100;
let mut rng = StdRng::seed_from_u64(88888);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let ivf_index = IvfRabitqIndex::train(
&data,
16,
7,
Metric::L2,
RotatorType::FhtKacRotator,
99999,
false,
)
.expect("train IVF");
let mut buffer = Vec::new();
ivf_index.save_to_writer(&mut buffer).expect("save IVF");
let loaded_ivf = RabitqIndex::load_from_reader(Cursor::new(&buffer)).expect("load IVF");
match loaded_ivf {
RabitqIndex::Ivf(idx) => {
let params = SearchParams::new(5, 16);
let results = idx.search(&data[0], params).expect("IVF search");
assert!(!results.is_empty(), "IVF search should return results");
assert_eq!(results[0].id, 0, "should find query vector");
}
RabitqIndex::BruteForce(_) => panic!("expected IVF index"),
}
let bf_index = BruteForceRabitqIndex::train(
&data,
7,
Metric::L2,
RotatorType::FhtKacRotator,
11111,
false,
)
.expect("train BruteForce");
let mut buffer = Vec::new();
bf_index
.save_to_writer(&mut buffer)
.expect("save BruteForce");
let loaded_bf = RabitqIndex::load_from_reader(Cursor::new(&buffer)).expect("load BruteForce");
match loaded_bf {
RabitqIndex::BruteForce(idx) => {
let params = BruteForceSearchParams::new(5);
let results = idx.search(&data[0], params).expect("BruteForce search");
assert!(
!results.is_empty(),
"BruteForce search should return results"
);
assert_eq!(results[0].id, 0, "should find query vector");
}
RabitqIndex::Ivf(_) => panic!("expected BruteForce index"),
}
}
#[test]
fn fastscan_matches_naive_bits3_l2() {
let dim = 48;
let total = 256;
let mut rng = StdRng::seed_from_u64(3333);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
32,
3, Metric::L2,
RotatorType::FhtKacRotator,
7777,
false,
)
.expect("train index");
let params = SearchParams::new(10, 16);
for (query_idx, query) in data.iter().take(10).enumerate() {
let fastscan = index.search(query, params).expect("fastscan search");
let naive = index.search_naive(query, params).expect("naive search");
assert_eq!(
fastscan.len(),
naive.len(),
"Result count mismatch for query {}",
query_idx
);
for (i, nv) in naive.iter().enumerate() {
let fs_match = fastscan.iter().find(|fs| fs.id == nv.id);
if let Some(fs) = fs_match {
let acceptable = float_comparison::score_acceptable_3bit(fs.score, nv.score);
assert!(
acceptable,
"Query {} pos {}: fastscan={:.6}, naive={:.6}, diff={:.6}",
query_idx,
i,
fs.score,
nv.score,
(fs.score - nv.score).abs()
);
} else {
if i >= params.top_k - 2 {
println!(
"Query {}: ID {} in naive pos {} not in FastScan top-{} (boundary effect)",
query_idx, nv.id, i, params.top_k
);
} else {
panic!(
"Query {}: ID {} missing from FastScan results at position {}",
query_idx, nv.id, i
);
}
}
}
}
}
#[test]
fn fastscan_matches_naive_bits3_ip() {
let dim = 32;
let total = 200;
let mut rng = StdRng::seed_from_u64(3344);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
25,
3, Metric::InnerProduct,
RotatorType::FhtKacRotator,
8888,
false,
)
.expect("train index");
let params = SearchParams::new(8, 12);
for (query_idx, query) in data.iter().take(10).enumerate() {
let fastscan = index.search(query, params).expect("fastscan search");
let naive = index.search_naive(query, params).expect("naive search");
assert_eq!(
fastscan.len(),
naive.len(),
"Result count mismatch for query {}",
query_idx
);
for (i, nv) in naive.iter().enumerate() {
let fs_match = fastscan.iter().find(|fs| fs.id == nv.id);
if let Some(fs) = fs_match {
let acceptable = float_comparison::score_acceptable_3bit(fs.score, nv.score);
assert!(
acceptable,
"Query {} pos {}: fastscan={:.6}, naive={:.6}, diff={:.6}",
query_idx,
i,
fs.score,
nv.score,
(fs.score - nv.score).abs()
);
} else if i >= params.top_k - 2 {
println!(
"Query {}: ID {} in naive pos {} not in FastScan top-{} (boundary effect)",
query_idx, nv.id, i, params.top_k
);
} else {
panic!(
"Query {}: ID {} missing from FastScan results at position {}",
query_idx, nv.id, i
);
}
}
}
}
#[test]
fn fastscan_matches_naive_bits7_l2() {
let dim = 64;
let total = 200;
let mut rng = StdRng::seed_from_u64(7777);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
20,
7, Metric::L2,
RotatorType::FhtKacRotator,
9999,
false,
)
.expect("train index");
let params = SearchParams::new(5, 12);
for (query_idx, query) in data.iter().take(8).enumerate() {
let fastscan = index.search(query, params).expect("fastscan search");
let naive = index.search_naive(query, params).expect("naive search");
assert_eq!(
fastscan.len(),
naive.len(),
"Result count mismatch for query {}",
query_idx
);
for (i, nv) in naive.iter().enumerate() {
let fs_match = fastscan.iter().find(|fs| fs.id == nv.id);
if let Some(fs) = fs_match {
let score_diff = (fs.score - nv.score).abs();
let acceptable = if nv.score.abs() < 0.1 {
score_diff < 0.2 } else {
let rel_error = score_diff / nv.score.abs();
rel_error < 0.02 || score_diff < 0.2
};
assert!(
acceptable,
"Query {} pos {}: fastscan={:.6}, naive={:.6}, diff={:.6}",
query_idx, i, fs.score, nv.score, score_diff
);
} else if i >= params.top_k - 2 {
println!(
"Query {}: ID {} in naive pos {} not in FastScan top-{} (boundary effect)",
query_idx, nv.id, i, params.top_k
);
} else {
panic!(
"Query {}: ID {} missing from FastScan results at position {}",
query_idx, nv.id, i
);
}
}
}
}
#[test]
fn fastscan_matches_naive_bits7_ip() {
let dim = 56;
let total = 180;
let mut rng = StdRng::seed_from_u64(7788);
let mut data = Vec::with_capacity(total);
for _ in 0..total {
data.push(random_vector(dim, &mut rng));
}
let index = IvfRabitqIndex::train(
&data,
18,
7, Metric::InnerProduct,
RotatorType::FhtKacRotator,
1111,
false,
)
.expect("train index");
let params = SearchParams::new(6, 10);
for (query_idx, query) in data.iter().take(8).enumerate() {
let fastscan = index.search(query, params).expect("fastscan search");
let naive = index.search_naive(query, params).expect("naive search");
assert_eq!(
fastscan.len(),
naive.len(),
"Result count mismatch for query {}",
query_idx
);
for (i, nv) in naive.iter().enumerate() {
let fs_match = fastscan.iter().find(|fs| fs.id == nv.id);
if let Some(fs) = fs_match {
let acceptable = float_comparison::score_acceptable_7bit(fs.score, nv.score);
assert!(
acceptable,
"Query {} pos {}: fastscan={:.6}, naive={:.6}, diff={:.6}",
query_idx,
i,
fs.score,
nv.score,
(fs.score - nv.score).abs()
);
} else if i >= params.top_k - 2 {
println!(
"Query {}: ID {} in naive pos {} not in FastScan top-{} (boundary effect)",
query_idx, nv.id, i, params.top_k
);
} else {
panic!(
"Query {}: ID {} missing from FastScan results at position {}",
query_idx, nv.id, i
);
}
}
}
}
#[test]
fn test_simple_serialization() {
let mut rng = StdRng::seed_from_u64(42);
let dim = 16;
let num_clusters = 2;
let num_vectors = 10;
let vectors: Vec<Vec<f32>> = (0..num_vectors)
.map(|_| (0..dim).map(|_| rng.gen::<f32>() * 10.0 - 5.0).collect())
.collect();
let index = IvfRabitqIndex::train(
&vectors,
num_clusters,
3, Metric::L2,
RotatorType::FhtKacRotator,
42, false, )
.expect("Failed to train index");
let mut buffer = Vec::new();
index.save_to_writer(&mut buffer).expect("Failed to save");
eprintln!("Saved buffer size: {} bytes", buffer.len());
let loaded_index = IvfRabitqIndex::load_from_reader(&buffer[..]).expect("Failed to load");
assert_eq!(index.len(), loaded_index.len());
}
#[test]
fn test_fetch_embedding_reconstruction() {
let dim = 64;
let mut rng = StdRng::seed_from_u64(42);
let num_vectors = 100;
let data: Vec<Vec<f32>> = (0..num_vectors)
.map(|_| random_vector(dim, &mut rng))
.collect();
let index = IvfRabitqIndex::train(
&data,
4, 7, Metric::L2,
RotatorType::MatrixRotator,
12345, false, )
.expect("Failed to train index");
for (vector_id, original) in data.iter().enumerate() {
let reconstructed = index
.fetch_embedding(vector_id)
.unwrap_or_else(|| panic!("Failed to fetch embedding for vector {}", vector_id));
assert_eq!(
reconstructed.len(),
dim,
"Reconstructed vector has wrong dimension"
);
let mut diff_sqr = 0.0f32;
let mut norm_sqr = 0.0f32;
for (a, b) in original.iter().zip(reconstructed.iter()) {
let diff = a - b;
diff_sqr += diff * diff;
norm_sqr += a * a;
}
let rel_error = if norm_sqr > f32::EPSILON {
(diff_sqr.sqrt()) / norm_sqr.sqrt()
} else {
0.0
};
assert!(
rel_error < 2.0,
"Reconstruction error too high for vector {}: relative error = {} (expected < 2.0)",
vector_id,
rel_error
);
}
assert!(
index.fetch_embedding(num_vectors + 10).is_none(),
"fetch_embedding should return None for non-existent ID"
);
}
#[test]
fn test_fetch_embedding_fht_rotator() {
let dim = 128; let mut rng = StdRng::seed_from_u64(99);
let num_vectors = 50;
let data: Vec<Vec<f32>> = (0..num_vectors)
.map(|_| random_vector(dim, &mut rng))
.collect();
let index = IvfRabitqIndex::train(
&data,
8, 7, Metric::L2,
RotatorType::FhtKacRotator, 54321, false,
)
.expect("Failed to train index with FHT rotator");
for _ in 0..10 {
let vector_id = rng.gen_range(0..num_vectors);
let original = &data[vector_id];
let reconstructed = index
.fetch_embedding(vector_id)
.unwrap_or_else(|| panic!("Failed to fetch embedding for vector {}", vector_id));
assert_eq!(reconstructed.len(), dim);
let error: f32 = original
.iter()
.zip(reconstructed.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f32>()
.sqrt();
let norm: f32 = original.iter().map(|x| x * x).sum::<f32>().sqrt();
let rel_error = error / norm.max(f32::EPSILON);
assert!(
rel_error < 2.0,
"FHT: Reconstruction error too high: {} (expected < 2.0)",
rel_error
);
}
}
#[test]
fn debug_quantization_factors() {
use crate::math::{dot, l2_distance_sqr, l2_norm_sqr};
use crate::quantizer::{quantize_with_centroid, RabitqConfig};
use crate::rotation::{DynamicRotator, RotatorType};
println!("\n=== Quantization Debug Test ===");
println!("Platform: {}", std::env::consts::ARCH);
let dim = 64;
let mut data = vec![0.0; dim];
data[0] = 1.0;
let centroid = vec![0.0; dim];
println!("Original vector: {:?}", data);
println!("Original centroid: {:?}", centroid);
println!("Original dim: {}", dim);
println!("\n=== Basic Math Operations ===");
let norm = l2_norm_sqr(&data);
println!("l2_norm_sqr(data) = {} (expected: 1.0)", norm);
assert!((norm - 1.0).abs() < 0.001, "Basic L2 norm is wrong!");
let dist = l2_distance_sqr(&data, ¢roid);
println!("l2_distance_sqr(data, centroid) = {} (expected: 1.0)", dist);
assert!((dist - 1.0).abs() < 0.001, "Basic L2 distance is wrong!");
let dot_val = dot(&data, ¢roid);
println!("dot(data, centroid) = {} (expected: 0.0)", dot_val);
assert!(dot_val.abs() < 0.001, "Basic dot product is wrong!");
println!("\n=== With Rotation (FHT-Kac) ===");
let rotator = DynamicRotator::new(dim, RotatorType::FhtKacRotator, 42);
let rotated = rotator.rotate(&data);
let rotated_dim = rotated.len(); println!(
"Rotated vector (first 16): {:?}",
&rotated[..rotated_dim.min(16)]
);
println!("Rotated dimension: {} (original: {})", rotated_dim, dim);
let rotated_norm = l2_norm_sqr(&rotated);
println!(
"l2_norm_sqr(rotated) = {} (should be ~1.0 for orthonormal rotation)",
rotated_norm
);
assert!(
(rotated_norm - 1.0).abs() < 0.01,
"Rotation is not orthonormal! norm^2 = {}",
rotated_norm
);
let rotated_centroid = rotator.rotate(¢roid);
let mut residual = vec![0.0f32; rotated_dim];
for i in 0..rotated_dim {
residual[i] = rotated[i] - rotated_centroid[i];
}
println!(
"Residual (first 16): {:?}",
&residual[..rotated_dim.min(16)]
);
println!("\n=== Quantization with 7 bits (L2) ===");
let config = RabitqConfig::new(7);
let quantized = quantize_with_centroid(&residual, &rotated_centroid, &config, Metric::L2);
println!("Ex bits: {}", quantized.ex_bits);
println!("\nQuantization Factors:");
println!(" delta: {:.10}", quantized.delta);
println!(" vl: {:.10}", quantized.vl);
println!(" f_add: {:.10}", quantized.f_add);
println!(" f_rescale: {:.10}", quantized.f_rescale);
println!(" f_error: {:.10}", quantized.f_error);
println!(" residual_norm: {:.10}", quantized.residual_norm);
println!(" f_add_ex: {:.10}", quantized.f_add_ex);
println!(" f_rescale_ex: {:.10}", quantized.f_rescale_ex);
assert!(quantized.f_add.is_finite(), "f_add is not finite!");
assert!(quantized.f_rescale.is_finite(), "f_rescale is not finite!");
assert!(quantized.f_add_ex.is_finite(), "f_add_ex is not finite!");
assert!(
quantized.f_rescale_ex.is_finite(),
"f_rescale_ex is not finite!"
);
println!("\n=== Simulated Distance Calculation (Self-Query) ===");
let query = &residual;
let ex_unpacked = quantized.unpack_ex_code();
let mut ex_dot = 0.0f32;
for i in 0..rotated_dim {
ex_dot += query[i] * ex_unpacked[i] as f32;
}
println!("Extended dot product: {:.6}", ex_dot);
let binary_unpacked = quantized.unpack_binary_code();
let mut binary_dot = 0.0f32;
for i in 0..rotated_dim {
let bit = if binary_unpacked[i] > 0 { 1.0 } else { -1.0 };
binary_dot += query[i] * bit;
}
println!("Binary dot product: {:.6}", binary_dot);
let g_add = l2_distance_sqr(query, &rotated_centroid);
println!("g_add: {:.6}", g_add);
let total_term = binary_dot * quantized.delta + ex_dot;
let distance = quantized.f_add_ex + g_add + quantized.f_rescale_ex * total_term;
println!("total_term: {:.6}", total_term);
println!("Estimated distance: {:.6}", distance);
if distance < 0.0 {
println!("\n❌ ERROR: Distance is NEGATIVE!");
println!("This is the ARM64 bug we're looking for!");
println!("\nDetailed breakdown:");
println!(" f_add_ex = {:.10}", quantized.f_add_ex);
println!(" g_add = {:.10}", g_add);
println!(" f_rescale_ex = {:.10}", quantized.f_rescale_ex);
println!(" total_term = {:.10}", total_term);
println!(" product = {:.10}", quantized.f_rescale_ex * total_term);
}
#[cfg(target_arch = "x86_64")]
{
assert!(
(0.0..10.0).contains(&distance),
"Distance should be non-negative and small for self-query, got {}",
distance
);
println!("\n✅ x86_64: Distance is correct");
}
#[cfg(not(target_arch = "x86_64"))]
{
if distance < 0.0 {
println!("\n⚠️ ARM64 BUG REPRODUCED: Distance is negative!");
} else {
println!("\n✅ ARM64: Distance is non-negative (bug may be fixed?)");
}
}
}
#[test]
fn debug_distance_formula_components() {
use crate::math::{dot, l2_distance_sqr, l2_norm_sqr};
println!("\n=== Testing Distance Formula Components ===");
println!("Platform: {}", std::env::consts::ARCH);
let _dim = 4;
let v1 = vec![1.0, 0.0, 0.0, 0.0];
let v2 = vec![1.0, 0.0, 0.0, 0.0];
let dist = l2_distance_sqr(&v1, &v2);
println!("\nTest 1: Identical vectors");
println!(" v1 = {:?}", v1);
println!(" v2 = {:?}", v2);
println!(" distance^2 = {} (expected: 0.0)", dist);
assert!(
dist.abs() < 1e-6,
"Distance between identical vectors should be 0"
);
let v1 = vec![1.0, 0.0, 0.0, 0.0];
let v2 = vec![0.0, 1.0, 0.0, 0.0];
let dist = l2_distance_sqr(&v1, &v2);
println!("\nTest 2: Orthogonal unit vectors");
println!(" v1 = {:?}", v1);
println!(" v2 = {:?}", v2);
println!(" distance^2 = {} (expected: 2.0)", dist);
assert!((dist - 2.0).abs() < 1e-6, "Distance should be sqrt(2)");
let v1 = vec![1.0, 0.0, 0.0, 0.0];
let v2 = vec![-1.0, 0.0, 0.0, 0.0];
let dist = l2_distance_sqr(&v1, &v2);
println!("\nTest 3: Opposite unit vectors");
println!(" v1 = {:?}", v1);
println!(" v2 = {:?}", v2);
println!(" distance^2 = {} (expected: 4.0)", dist);
assert!((dist - 4.0).abs() < 1e-6, "Distance should be 2");
let v1 = vec![0.5, 0.5, 0.5, 0.5];
let v2 = vec![0.3, 0.4, 0.5, 0.6];
let dist_direct = l2_distance_sqr(&v1, &v2);
let norm1 = l2_norm_sqr(&v1);
let norm2 = l2_norm_sqr(&v2);
let dot_prod = dot(&v1, &v2);
let dist_formula = norm1 + norm2 - 2.0 * dot_prod;
println!("\nTest 4: Formula verification");
println!(" v1 = {:?}", v1);
println!(" v2 = {:?}", v2);
println!(" Direct distance^2 = {}", dist_direct);
println!(" ||v1||^2 = {}", norm1);
println!(" ||v2||^2 = {}", norm2);
println!(" <v1,v2> = {}", dot_prod);
println!(
" Formula: ||v1||^2 + ||v2||^2 - 2<v1,v2> = {}",
dist_formula
);
println!(" Difference: {}", (dist_direct - dist_formula).abs());
assert!(
(dist_direct - dist_formula).abs() < 1e-5,
"Distance formula mismatch: direct={}, formula={}",
dist_direct,
dist_formula
);
println!("\n✅ All basic distance formula tests passed!");
}