use ann_search_rs::*;
use faer::{Mat, MatRef};
use rayon::prelude::*;
use crate::prelude::*;
const NORM_ROW_BLOCK: usize = 4096;
pub fn cosine_normalise(mat: &Mat<f32>) -> Mat<f32> {
let nrows = mat.nrows();
let ncols = mat.ncols();
let mut norms = vec![0.0f32; nrows];
norms
.par_chunks_mut(NORM_ROW_BLOCK)
.enumerate()
.for_each(|(block, acc)| {
let lo = block * NORM_ROW_BLOCK;
for col in 0..ncols {
for (i, a) in acc.iter_mut().enumerate() {
let v = mat[(lo + i, col)];
*a += v * v;
}
}
acc.iter_mut().for_each(|a| *a = a.sqrt());
});
let mut out = Mat::<f32>::zeros(nrows, ncols);
par_for_each_col_mut(&mut out, |col, dst| {
for (row, d) in dst.iter_mut().enumerate() {
let norm = norms[row];
*d = if norm > 1e-8 {
mat[(row, col)] / norm
} else {
0.0 };
}
});
out
}
pub(crate) fn par_for_each_col_mut<F>(mat: &mut Mat<f32>, f: F)
where
F: Fn(usize, &mut [f32]) + Sync,
{
mat.as_mut()
.par_col_chunks_mut(1)
.enumerate()
.for_each(|(j, col)| {
f(
j,
col.col_mut(0)
.try_as_col_major_mut()
.expect("owned Mat columns are contiguous")
.as_slice_mut(),
)
});
}
pub fn process_batch_labels(batch_labels: &[usize]) -> (Vec<usize>, usize) {
let unique_batches: Vec<usize> = {
let mut batches: Vec<_> = batch_labels.to_vec();
batches.sort_unstable();
batches.dedup();
batches
};
let n_batches = unique_batches.len();
(unique_batches, n_batches)
}
pub fn standardise_per_column(mat: MatRef<f32>) -> Mat<f32> {
let n_features = mat.nrows();
let n_cells = mat.ncols();
let stats: Vec<(f32, f32)> = (0..n_cells)
.into_par_iter()
.map(|col| {
let mut sum = 0.0_f32;
for row in 0..n_features {
sum += *mat.get(row, col);
}
let mean = sum / n_features as f32;
let mut ss = 0.0_f32;
for row in 0..n_features {
let d = *mat.get(row, col) - mean;
ss += d * d;
}
let sd = (ss / (n_features as f32 - 1.0).max(1.0)).sqrt();
(mean, sd)
})
.collect();
let mut out = Mat::<f32>::zeros(n_features, n_cells);
par_for_each_col_mut(&mut out, |col, dst| {
let (mean, sd) = stats[col];
for (row, d) in dst.iter_mut().enumerate() {
*d = if sd > 1e-15 {
(*mat.get(row, col) - mean) / sd
} else {
0.0
};
}
});
out
}
pub fn batch_knn_search(
query: MatRef<f32>,
reference: MatRef<f32>,
k: usize,
params: &KnnParams,
seed: usize,
verbose: usize,
) -> ScKnnResults {
let verbosity = parse_verbosity_level(verbose);
let knn_method: KnnSearch = parse_knn_method(¶ms.knn_method).unwrap_or_default();
let (indices, dist) = match knn_method {
KnnSearch::Hnsw => {
let index = build_hnsw_index(
reference,
params.m,
params.ef_construction,
¶ms.ann_dist,
seed,
verbosity.detailed_verbosity(),
);
query_hnsw_index(
query,
&index,
k,
params.ef_search,
true,
verbosity.detailed_verbosity(),
)?
}
KnnSearch::Annoy => {
let index = build_annoy_index(reference, ¶ms.ann_dist, params.n_tree, seed)?;
query_annoy_index(
query,
&index,
k,
params.search_budget,
true,
verbosity.detailed_verbosity(),
)?
}
KnnSearch::NNDescent => {
let index = build_nndescent_index(
reference,
¶ms.ann_dist,
params.delta,
params.diversify_prob,
None,
None,
None,
None,
seed,
verbosity.detailed_verbosity(),
)?;
query_nndescent_index(
query,
&index,
k,
params.ef_budget,
true,
verbosity.detailed_verbosity(),
)?
}
KnnSearch::Exhaustive => {
let index = build_exhaustive_index(reference, ¶ms.ann_dist);
query_exhaustive_index(query, &index, k, true, verbosity.detailed_verbosity())?
}
KnnSearch::Ivf => {
let index = build_ivf_index(
reference,
params.n_list,
None,
¶ms.ann_dist,
seed,
verbosity.detailed_verbosity(),
)?;
query_ivf_index(
query,
&index,
k,
params.n_list,
true,
verbosity.detailed_verbosity(),
)?
}
KnnSearch::KmKnn => {
let index = build_kmknn_index(
reference,
¶ms.ann_dist,
params.n_list,
None,
seed,
verbosity.detailed_verbosity(),
)?;
query_kmknn_index(query, &index, k, true, verbosity.detailed_verbosity())?
}
};
let mut dist = dist.expect("distances were requested from the index");
to_true_distances(&mut dist, ¶ms.ann_dist);
Ok((indices, dist))
}
#[cfg(test)]
mod tests_batch_utils {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_cosine_normalise_basic() {
let mat = Mat::from_fn(3, 3, |i, j| {
match i {
0 => [3.0, 4.0, 0.0][j], 1 => [1.0, 0.0, 0.0][j], 2 => [0.5, 0.5, 0.5][j], _ => unreachable!(),
}
});
let normalised = cosine_normalise(&mat);
for row in 0..3 {
let norm: f32 = (0..3)
.map(|col| normalised[(row, col)].powi(2))
.sum::<f32>()
.sqrt();
assert_relative_eq!(norm, 1.0, epsilon = 1e-6);
}
assert_relative_eq!(normalised[(0, 0)], 3.0 / 5.0, epsilon = 1e-6);
assert_relative_eq!(normalised[(0, 1)], 4.0 / 5.0, epsilon = 1e-6);
assert_relative_eq!(normalised[(1, 0)], 1.0, epsilon = 1e-6);
}
#[test]
fn test_cosine_normalise_near_zero_threshold() {
let mat = Mat::from_fn(3, 3, |i, j| {
match i {
0 => [1e-9, 1e-9, 1e-9][j], 1 => [1e-7, 1e-7, 1e-7][j], 2 => [1e-8, 0.0, 0.0][j], _ => unreachable!(),
}
});
let normalized = cosine_normalise(&mat);
for col in 0..3 {
assert_eq!(normalized[(0, col)], 0.0);
}
let norm: f32 = (0..3)
.map(|col| normalized[(1, col)].powi(2))
.sum::<f32>()
.sqrt();
assert_relative_eq!(norm, 1.0, epsilon = 1e-4);
for col in 0..3 {
assert_eq!(normalized[(2, col)], 0.0);
}
}
#[test]
fn test_cosine_normalise_single_column() {
let mat = Mat::from_fn(3, 1, |i, _| match i {
0 => 5.0,
1 => 0.0,
2 => -3.0,
_ => unreachable!(),
});
let normalised = cosine_normalise(&mat);
assert_relative_eq!(normalised[(0, 0)], 1.0, epsilon = 1e-6);
assert_eq!(normalised[(1, 0)], 0.0);
assert_relative_eq!(normalised[(2, 0)], -1.0, epsilon = 1e-6);
}
#[test]
fn test_cosine_normalise_mixed_cases() {
let mat = Mat::from_fn(5, 4, |i, j| {
match i {
0 => [3.0, 4.0, 0.0, 0.0][j], 1 => [0.0, 0.0, 0.0, 0.0][j], 2 => [1.0, 1.0, 1.0, 1.0][j], 3 => [100.0, 200.0, 300.0, 0.0][j], 4 => [0.001, 0.002, 0.002, 0.0][j], _ => unreachable!(),
}
});
let normalised = cosine_normalise(&mat);
for col in 0..4 {
assert_eq!(normalised[(1, col)], 0.0);
}
for row in [0, 2, 3, 4] {
let norm: f32 = (0..4)
.map(|col| normalised[(row, col)].powi(2))
.sum::<f32>()
.sqrt();
assert_relative_eq!(norm, 1.0, epsilon = 1e-5);
}
}
}