use crate::config::{DistanceMetric, KnnMethod};
use crate::error::PaCMAPError;
use rayon::prelude::*;
pub struct NeighborList {
pub indices: Vec<u32>,
pub distances: Vec<f32>,
}
pub fn compute_knn(
data: &[f32],
n: usize,
d: usize,
k: usize,
method: &KnnMethod,
metric: DistanceMetric,
) -> Result<Vec<NeighborList>, PaCMAPError> {
match method {
#[cfg(feature = "hnsw")]
KnnMethod::Hnsw(params) => hnsw_knn(data, n, d, k, params, metric),
KnnMethod::Exact => exact_knn(data, n, d, k, metric),
#[cfg(feature = "kdtree")]
KnnMethod::KdTree => kdtree_knn(data, n, d, k, metric),
KnnMethod::Annoy => Err(PaCMAPError::MethodNotImplemented {
method: "Annoy".to_string(),
}),
#[allow(unreachable_patterns)]
_ => Err(PaCMAPError::MethodNotImplemented {
method: "unknown (feature disabled)".to_string(),
}),
}
}
#[inline(always)]
fn l2_sq(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
}
#[inline(always)]
fn dist(a: &[f32], b: &[f32], metric: DistanceMetric) -> f32 {
match metric {
DistanceMetric::Euclidean => l2_sq(a, b).sqrt(),
DistanceMetric::EuclideanSq => l2_sq(a, b),
DistanceMetric::Cosine => {
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
let na: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let nb: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
1.0 - dot / (na * nb + f32::EPSILON)
}
DistanceMetric::Manhattan => a.iter().zip(b).map(|(x, y)| (x - y).abs()).sum(),
}
}
pub fn exact_knn(
data: &[f32],
n: usize,
d: usize,
k: usize,
metric: DistanceMetric,
) -> Result<Vec<NeighborList>, PaCMAPError> {
let k_capped = k.min(n - 1);
let result: Vec<NeighborList> = (0..n)
.into_par_iter()
.map(|i| {
let row_i = &data[i * d..(i + 1) * d];
let mut heap: Vec<(f32, u32)> = Vec::with_capacity(k_capped + 1);
for j in 0..n {
if j == i {
continue;
}
let row_j = &data[j * d..(j + 1) * d];
let d_ij = dist(row_i, row_j, metric);
if heap.len() < k_capped {
heap.push((d_ij, j as u32));
heap.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
} else if d_ij < heap[0].0 {
heap[0] = (d_ij, j as u32);
heap.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
}
}
heap.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
NeighborList {
indices: heap.iter().map(|(_, idx)| *idx).collect(),
distances: heap.iter().map(|(d, _)| *d).collect(),
}
})
.collect();
Ok(result)
}
#[cfg(feature = "hnsw")]
fn hnsw_knn(
data: &[f32],
n: usize,
d: usize,
k: usize,
params: &crate::config::HnswParams,
metric: DistanceMetric,
) -> Result<Vec<NeighborList>, PaCMAPError> {
use usearch::{Index, IndexOptions, MetricKind, ScalarKind};
if metric == DistanceMetric::Manhattan {
return exact_knn(data, n, d, k, metric);
}
let metric_kind = match metric {
DistanceMetric::Euclidean | DistanceMetric::EuclideanSq => MetricKind::L2sq,
DistanceMetric::Cosine => MetricKind::Cos,
DistanceMetric::Manhattan => unreachable!(),
};
let scalar_kind = match params.quantization {
crate::config::Quantization::F32 => ScalarKind::F32,
crate::config::Quantization::F16 => ScalarKind::F16,
crate::config::Quantization::I8 => ScalarKind::I8,
};
let options = IndexOptions {
dimensions: d,
metric: metric_kind,
quantization: scalar_kind,
connectivity: params.m,
expansion_add: params.ef_construction,
expansion_search: params.ef_search,
..Default::default()
};
let index = Index::new(&options)
.map_err(|e| PaCMAPError::KnnIndex(e.to_string()))?;
index.reserve(n)
.map_err(|e| PaCMAPError::KnnIndex(e.to_string()))?;
(0..n).into_par_iter().try_for_each(|i| {
let row = &data[i * d..(i + 1) * d];
index.add(i as u64, row)
.map_err(|e| PaCMAPError::KnnIndex(e.to_string()))
})?;
let k_fetch = k + 1; let result: Vec<NeighborList> = (0..n)
.into_par_iter()
.map(|i| {
let row = &data[i * d..(i + 1) * d];
let matches = index.search(row, k_fetch).unwrap_or_else(|_| {
usearch::ffi::Matches {
keys: vec![],
distances: vec![],
}
});
let mut indices = Vec::with_capacity(k);
let mut distances = Vec::with_capacity(k);
for (&key, &dist) in matches.keys.iter().zip(matches.distances.iter()) {
if key == i as u64 {
continue;
}
if indices.len() >= k {
break;
}
indices.push(key as u32);
distances.push(dist);
}
NeighborList { indices, distances }
})
.collect();
drop(index);
Ok(result)
}
#[cfg(feature = "kdtree")]
fn kdtree_knn(
data: &[f32],
n: usize,
d: usize,
k: usize,
_metric: DistanceMetric,
) -> Result<Vec<NeighborList>, PaCMAPError> {
exact_knn(data, n, d, k, _metric)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_grid(n: usize) -> Vec<f32> {
(0..n).flat_map(|i| [i as f32, 0.0]).collect()
}
#[test]
fn exact_knn_nearest_neighbour() {
let data = make_grid(10);
let knn = exact_knn(&data, 10, 2, 2, DistanceMetric::Euclidean).unwrap();
let nbrs = &knn[5].indices;
assert!(nbrs.contains(&4u32) || nbrs.contains(&6u32));
}
}