use rayon::prelude::*;
use crate::util::{
assert_all_finite, cmp_finite_f32_then_index, l2_normalise, result_buffer_len, TopK,
};
use crate::SearchResults;
pub fn rank_transform(v: &[f32]) -> Vec<u16> {
let d = v.len();
assert!(d <= u16::MAX as usize, "dim must fit in u16");
assert_all_finite(v);
let mut order: Vec<u16> = (0..d as u16).collect();
order.sort_unstable_by(|&lhs, &rhs| {
let lhs = lhs as usize;
let rhs = rhs as usize;
cmp_finite_f32_then_index(v[lhs], lhs, v[rhs], rhs)
});
let mut ranks = vec![0u16; d];
for (rank, &orig_idx) in order.iter().enumerate() {
ranks[orig_idx as usize] = rank as u16;
}
ranks
}
pub fn rank_transform_into(v: &[f32], out: &mut [u16]) {
let d = v.len();
assert_eq!(d, out.len(), "out must have the same length as v");
assert!(d <= u16::MAX as usize, "dim must fit in u16");
assert_all_finite(v);
let mut order: Vec<u16> = (0..d as u16).collect();
order.sort_unstable_by(|&lhs, &rhs| {
let lhs = lhs as usize;
let rhs = rhs as usize;
cmp_finite_f32_then_index(v[lhs], lhs, v[rhs], rhs)
});
for (rank, &orig_idx) in order.iter().enumerate() {
out[orig_idx as usize] = rank as u16;
}
}
#[inline]
pub fn rank_to_bucket(rank: u16, d: usize, bits: u8) -> u8 {
assert!(bits <= 7, "bits too large");
assert!(d > 0, "d must be positive");
assert!((rank as usize) < d, "rank ({rank}) must be < d ({d})");
let n_buckets = 1u32 << bits;
((rank as u64 * n_buckets as u64) / d as u64) as u8
}
pub fn bucket_ranks(ranks: &[u16], bits: u8) -> Vec<u8> {
assert!(bits <= 7, "bits too large");
let d = ranks.len();
ranks.iter().map(|&r| rank_to_bucket(r, d, bits)).collect()
}
pub fn pack_buckets(buckets: &[u8], bits: u8) -> Vec<u8> {
assert!(matches!(bits, 1 | 2 | 4), "bits must be 1, 2, or 4");
let codes_per_byte = (8 / bits) as usize;
let d = buckets.len();
assert_eq!(
d % codes_per_byte,
0,
"d ({d}) must be a multiple of codes_per_byte ({codes_per_byte}) for bits = {bits}",
);
let mask = (1u8 << bits) - 1;
let n_bytes = d / codes_per_byte;
let mut out = vec![0u8; n_bytes];
let bits_u = bits as usize;
for (i, &b) in buckets.iter().enumerate() {
assert!(
b <= mask,
"bucket code {b} out of range: every code must be < 1 << bits ({})",
mask as u16 + 1,
);
let byte_idx = i / codes_per_byte;
let pos = i % codes_per_byte;
let shift = (codes_per_byte - 1 - pos) * bits_u;
out[byte_idx] |= b << shift;
}
out
}
pub fn unpack_buckets(packed: &[u8], d: usize, bits: u8) -> Vec<u8> {
assert!(matches!(bits, 1 | 2 | 4), "bits must be 1, 2, or 4");
let codes_per_byte = (8 / bits) as usize;
assert_eq!(packed.len() * codes_per_byte, d);
let mask = (1u8 << bits) - 1;
let bits_u = bits as usize;
let mut out = vec![0u8; d];
#[allow(clippy::needless_range_loop)] for i in 0..d {
let byte_idx = i / codes_per_byte;
let pos = i % codes_per_byte;
let shift = (codes_per_byte - 1 - pos) * bits_u;
out[i] = (packed[byte_idx] >> shift) & mask;
}
out
}
#[inline]
pub fn rankquant_bytes_per_vec(d: usize, bits: u8) -> usize {
assert!(matches!(bits, 1 | 2 | 4), "bits must be 1,2,4");
let codes_per_byte = (8 / bits) as usize;
assert_eq!(
d % codes_per_byte,
0,
"d ({d}) must be a multiple of codes_per_byte ({codes_per_byte}) for bits = {bits}"
);
d / codes_per_byte
}
#[inline]
pub fn bucket_centre(bucket: u8, bits: u8) -> f32 {
assert!(bits <= 7, "bits too large");
assert!(
(bucket as u32) < (1u32 << bits),
"bucket {bucket} out of range for bits = {bits}",
);
let n = (1u32 << bits) as f32;
bucket as f32 - (n - 1.0) / 2.0
}
#[inline]
pub fn rank_norm(d: usize) -> f32 {
let d = d as f64;
(d * (d * d - 1.0) / 12.0).sqrt() as f32
}
#[inline]
pub fn rankquant_norm(d: usize, bits: u8) -> f32 {
assert!(matches!(bits, 1 | 2 | 4), "bits must be 1,2,4");
let n = (1u32 << bits) as f64;
let var = (n * n - 1.0) / 12.0;
((d as f64) * var).sqrt() as f32
}
pub struct Rank {
dim: usize,
n_vectors: usize,
ranks: Vec<u16>,
}
impl Rank {
pub fn new(dim: usize) -> Self {
assert!(dim >= 2, "dim must be >= 2");
assert!(dim <= u16::MAX as usize, "dim must fit in u16");
Self {
dim,
n_vectors: 0,
ranks: Vec::new(),
}
}
pub fn add(&mut self, vectors: &[f32]) {
let n = vectors.len() / self.dim;
assert_eq!(
vectors.len(),
n * self.dim,
"vectors length must be a multiple of dim",
);
assert_all_finite(vectors);
let new_n = crate::util::checked_new_len(self.n_vectors, n, self.dim);
let start = self.ranks.len();
self.ranks.resize(start + n * self.dim, 0);
let dim = self.dim;
self.ranks[start..]
.par_chunks_mut(dim)
.zip(vectors.par_chunks(dim))
.for_each(|(out, v)| rank_transform_into(v, out));
self.n_vectors = new_n;
}
pub fn search(&self, queries: &[f32], k: usize) -> SearchResults {
let nq = queries.len() / self.dim;
assert_eq!(queries.len(), nq * self.dim);
assert_all_finite(queries);
let k = k.min(self.n_vectors);
let k_eff = k;
let buf_len = result_buffer_len(nq, k);
if k_eff == 0 {
return SearchResults {
scores: vec![0.0; buf_len],
indices: vec![-1; buf_len],
nq,
k,
};
}
let dim = self.dim;
let mean_2x = (dim as i32) - 1; let n = self.n_vectors;
let norm = rank_norm(dim);
let inv_norm_sq = 1.0_f32 / (norm * norm);
let mut scores_flat = vec![0.0f32; buf_len];
let mut indices_flat = vec![-1i64; buf_len];
queries
.par_chunks(dim)
.zip(scores_flat.par_chunks_mut(k))
.zip(indices_flat.par_chunks_mut(k))
.for_each(|((q, out_scores), out_indices)| {
let q_ranks = rank_transform(q);
let mut top = TopK::new(k_eff);
for di in 0..n {
let doc = &self.ranks[di * dim..(di + 1) * dim];
let mut acc: i64 = 0;
for d in 0..dim {
let qc = 2 * (q_ranks[d] as i32) - mean_2x;
let dc = 2 * (doc[d] as i32) - mean_2x;
acc += (qc as i64) * (dc as i64);
}
let s = (acc as f32) * 0.25 * inv_norm_sq;
top.maybe_insert(s, di);
}
top.finalize_into(out_scores, out_indices);
});
SearchResults {
scores: scores_flat,
indices: indices_flat,
nq,
k,
}
}
pub fn search_asymmetric(&self, queries: &[f32], k: usize) -> SearchResults {
let nq = queries.len() / self.dim;
assert_eq!(queries.len(), nq * self.dim);
assert_all_finite(queries);
let k = k.min(self.n_vectors);
let k_eff = k;
let buf_len = result_buffer_len(nq, k);
if k_eff == 0 {
return SearchResults {
scores: vec![0.0; buf_len],
indices: vec![-1; buf_len],
nq,
k,
};
}
let dim = self.dim;
let n = self.n_vectors;
let norm = rank_norm(dim);
let inv_norm = 1.0_f32 / norm;
let mean = (dim as f32 - 1.0) / 2.0;
let mut scores_flat = vec![0.0f32; buf_len];
let mut indices_flat = vec![-1i64; buf_len];
queries
.par_chunks(dim)
.zip(scores_flat.par_chunks_mut(k))
.zip(indices_flat.par_chunks_mut(k))
.for_each(|((q, out_scores), out_indices)| {
let q_unit = l2_normalise(q);
let q_sum: f32 = q_unit.iter().sum();
let mut top = TopK::new(k_eff);
for di in 0..n {
let doc = &self.ranks[di * dim..(di + 1) * dim];
let mut acc = 0.0f32;
for d in 0..dim {
acc += q_unit[d] * doc[d] as f32;
}
let s = (acc - mean * q_sum) * inv_norm;
top.maybe_insert(s, di);
}
top.finalize_into(out_scores, out_indices);
});
SearchResults {
scores: scores_flat,
indices: indices_flat,
nq,
k,
}
}
pub fn len(&self) -> usize {
self.n_vectors
}
pub fn is_empty(&self) -> bool {
self.n_vectors == 0
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn bytes_per_vec(&self) -> usize {
self.dim * 2
}
pub fn byte_size(&self) -> usize {
self.ranks.len() * std::mem::size_of::<u16>()
}
pub fn swap_remove(&mut self, idx: usize) -> usize {
assert!(idx < self.n_vectors, "index out of bounds");
let last = self.n_vectors - 1;
let dim = self.dim;
if idx != last {
self.ranks
.copy_within(last * dim..last * dim + dim, idx * dim);
}
self.ranks.truncate(last * dim);
self.n_vectors -= 1;
last
}
pub fn write(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
crate::rank_io::write_rank(path, self.dim, self.n_vectors, &self.ranks)
}
pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let (dim, n_vectors, ranks) = crate::rank_io::load_rank(path)?;
let expected = n_vectors.checked_mul(dim).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"TVR1 n_vectors * dim overflows usize",
)
})?;
if ranks.len() != expected {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"TVR1 payload length does not match dim * n_vectors",
));
}
Ok(Self {
dim,
n_vectors,
ranks,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rank_transform_matches_numpy_argsort_argsort() {
let v = [3.0, 1.0, 4.0, 1.5, 5.0, 9.0, 2.0, 6.0];
let r = rank_transform(&v);
assert_eq!(r, vec![3, 0, 4, 1, 5, 7, 2, 6]);
}
#[test]
fn rank_transform_is_permutation() {
let v: Vec<f32> = (0..256).map(|i| (i as f32 * 7.0).sin()).collect();
let r = rank_transform(&v);
let mut sorted = r.clone();
sorted.sort();
let expected: Vec<u16> = (0..256u16).collect();
assert_eq!(sorted, expected);
}
#[test]
fn ties_broken_by_index() {
let v = [1.0_f32, 1.0, 1.0, 1.0];
let r = rank_transform(&v);
assert_eq!(r, vec![0, 1, 2, 3]);
}
#[test]
fn duplicate_values_tie_by_original_index() {
let v = [3.0_f32, 1.0, 3.0, 2.0, 1.0];
let r = rank_transform(&v);
assert_eq!(r, vec![3, 0, 4, 2, 1]);
}
#[test]
fn signed_zeroes_tie_by_original_index() {
let v = [0.0_f32, -0.0, 1.0, -0.0, 0.0];
let r = rank_transform(&v);
assert_eq!(r, vec![0, 1, 4, 2, 3]);
}
#[test]
fn rank_to_bucket_partitions_uniformly() {
let d = 1024;
let bits = 2u8;
let mut counts = [0usize; 4];
for rank in 0..d as u16 {
let b = rank_to_bucket(rank, d, bits);
counts[b as usize] += 1;
}
for c in counts {
assert_eq!(c, d / 4);
}
}
#[test]
#[cfg(target_pointer_width = "64")]
fn rank_to_bucket_large_d_does_not_divide_by_zero() {
let huge_d = 1usize << 40;
assert_eq!(rank_to_bucket(0, huge_d, 2), 0);
assert!(rank_to_bucket(u16::MAX, huge_d, 2) < 4);
}
#[test]
#[should_panic(expected = "must be < d")]
fn rank_to_bucket_rejects_rank_ge_d() {
let _ = rank_to_bucket(8, 8, 2);
}
#[test]
#[should_panic(expected = "bits too large")]
fn bucket_ranks_rejects_bits_above_7_even_when_empty() {
let _ = bucket_ranks(&[], 8);
}
#[test]
fn pack_unpack_round_trip_bits2() {
let buckets: Vec<u8> = (0..16).map(|i| (i % 4) as u8).collect();
let packed = pack_buckets(&buckets, 2);
assert_eq!(packed.len(), 4);
let unpacked = unpack_buckets(&packed, 16, 2);
assert_eq!(unpacked, buckets);
}
#[test]
fn pack_unpack_round_trip_bits1() {
let buckets: Vec<u8> = (0..16).map(|i| (i % 2) as u8).collect();
let packed = pack_buckets(&buckets, 1);
assert_eq!(packed.len(), 2);
let unpacked = unpack_buckets(&packed, 16, 1);
assert_eq!(unpacked, buckets);
}
#[test]
fn pack_unpack_round_trip_bits4() {
let buckets: Vec<u8> = (0..16).map(|i| (i % 16) as u8).collect();
let packed = pack_buckets(&buckets, 4);
assert_eq!(packed.len(), 8);
let unpacked = unpack_buckets(&packed, 16, 4);
assert_eq!(unpacked, buckets);
}
#[test]
fn bucket_centres_are_symmetric_around_zero() {
let centres: Vec<f32> = (0..4u8).map(|b| bucket_centre(b, 2)).collect();
assert_eq!(centres, vec![-1.5, -0.5, 0.5, 1.5]);
let sum: f32 = centres.iter().sum();
assert!(sum.abs() < 1e-6);
}
#[test]
fn rank_norm_matches_direct_computation() {
let d = 1024usize;
let analytical = rank_norm(d);
let direct: f32 = {
let mean = (d as f32 - 1.0) / 2.0;
let ss: f32 = (0..d)
.map(|i| {
let c = i as f32 - mean;
c * c
})
.sum();
ss.sqrt()
};
assert!(
(analytical - direct).abs() / direct < 1e-5,
"analytical {analytical}, direct {direct}"
);
}
#[test]
fn rankquant_norm_matches_direct_computation() {
let d = 1024usize;
let bits = 2u8;
let analytical = rankquant_norm(d, bits);
let ranks: Vec<u16> = (0..d as u16).collect();
let buckets = bucket_ranks(&ranks, bits);
let centred: Vec<f32> = buckets.iter().map(|&b| bucket_centre(b, bits)).collect();
let direct: f32 = centred.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!(
(analytical - direct).abs() / direct < 1e-5,
"analytical {analytical}, direct {direct}"
);
}
#[test]
#[should_panic(expected = "out of range")]
fn pack_buckets_rejects_out_of_range_code() {
let _ = pack_buckets(&[7, 7, 7, 7], 2);
}
#[test]
#[should_panic(expected = "bits too large")]
fn bucket_centre_rejects_bits_above_7() {
let _ = bucket_centre(0, 8);
}
#[test]
#[should_panic(expected = "out of range for bits")]
fn bucket_centre_rejects_out_of_range_bucket() {
let _ = bucket_centre(4, 2);
}
#[test]
#[should_panic(expected = "bits must be 1,2,4")]
fn rankquant_norm_rejects_invalid_bits() {
let _ = rankquant_norm(64, 3);
}
}