use std::fmt;
pub const LANES: usize = 16;
#[derive(Clone, PartialEq)]
pub struct PdxBlock {
data: Vec<f32>,
dim: usize,
len: usize,
}
impl fmt::Debug for PdxBlock {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PdxBlock")
.field("dim", &self.dim)
.field("len", &self.len)
.finish_non_exhaustive()
}
}
impl PdxBlock {
pub fn from_rows(vectors: &[&[f32]], dim: usize) -> Self {
assert!(
vectors.len() <= LANES,
"block holds at most {LANES} vectors, got {}",
vectors.len()
);
for (i, v) in vectors.iter().enumerate() {
assert_eq!(
v.len(),
dim,
"vector {i} has length {}, expected {dim}",
v.len()
);
}
let mut data = vec![0.0f32; dim * LANES];
for (lane, v) in vectors.iter().enumerate() {
for (d, &value) in v.iter().enumerate() {
data[d * LANES + lane] = value;
}
}
Self {
data,
dim,
len: vectors.len(),
}
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn euclidean_sq_into(&self, query: &[f32], out: &mut [f32]) {
assert_eq!(query.len(), self.dim, "query dimension mismatch");
assert!(out.len() >= self.len, "output buffer too small");
let mut acc = [0.0f32; LANES];
for (d, &q) in query.iter().enumerate() {
let row = &self.data[d * LANES..d * LANES + LANES];
for lane in 0..LANES {
let diff = row[lane] - q;
acc[lane] = diff.mul_add(diff, acc[lane]);
}
}
out[..self.len].copy_from_slice(&acc[..self.len]);
}
pub fn dot_into(&self, query: &[f32], out: &mut [f32]) {
assert_eq!(query.len(), self.dim, "query dimension mismatch");
assert!(out.len() >= self.len, "output buffer too small");
let mut acc = [0.0f32; LANES];
for (d, &q) in query.iter().enumerate() {
let row = &self.data[d * LANES..d * LANES + LANES];
for lane in 0..LANES {
acc[lane] = row[lane].mul_add(q, acc[lane]);
}
}
out[..self.len].copy_from_slice(&acc[..self.len]);
}
}
#[derive(Debug, Clone)]
pub struct PdxIndex {
blocks: Vec<PdxBlock>,
dim: usize,
len: usize,
}
impl PdxIndex {
pub fn from_rows(vectors: &[&[f32]]) -> Self {
assert!(
!vectors.is_empty(),
"cannot build a PdxIndex from no vectors"
);
let dim = vectors[0].len();
let blocks = vectors
.chunks(LANES)
.map(|chunk| PdxBlock::from_rows(chunk, dim))
.collect();
Self {
blocks,
dim,
len: vectors.len(),
}
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn block_count(&self) -> usize {
self.blocks.len()
}
pub fn euclidean_sq_into(&self, query: &[f32], out: &mut [f32]) {
assert!(out.len() >= self.len, "output buffer too small");
let mut written = 0;
for block in &self.blocks {
block.euclidean_sq_into(query, &mut out[written..]);
written += block.len();
}
}
pub fn dot_into(&self, query: &[f32], out: &mut [f32]) {
assert!(out.len() >= self.len, "output buffer too small");
let mut written = 0;
for block in &self.blocks {
block.dot_into(query, &mut out[written..]);
written += block.len();
}
}
pub fn euclidean_sq(&self, query: &[f32]) -> Vec<f32> {
let mut out = vec![0.0; self.len];
self.euclidean_sq_into(query, &mut out);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn euclidean_sq_ref(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
}
fn dot_ref(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b).map(|(x, y)| x * y).sum()
}
fn make(n: usize, dim: usize) -> Vec<Vec<f32>> {
(0..n)
.map(|i| {
(0..dim)
.map(|d| ((i * 31 + d * 17) % 101) as f32 / 101.0)
.collect()
})
.collect()
}
#[test]
fn block_matches_reference_euclidean() {
let dim = 128;
let vecs = make(LANES, dim);
let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
let block = PdxBlock::from_rows(&refs, dim);
let query: Vec<f32> = (0..dim).map(|d| (d % 7) as f32 / 7.0).collect();
let mut got = vec![0.0; LANES];
block.euclidean_sq_into(&query, &mut got);
for (i, v) in vecs.iter().enumerate() {
let want = euclidean_sq_ref(&query, v);
assert!(
(got[i] - want).abs() < 1e-4,
"lane {i}: got {}, want {want}",
got[i]
);
}
}
#[test]
fn block_matches_reference_dot() {
let dim = 96;
let vecs = make(LANES, dim);
let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
let block = PdxBlock::from_rows(&refs, dim);
let query: Vec<f32> = (0..dim).map(|d| (d % 5) as f32 / 5.0).collect();
let mut got = vec![0.0; LANES];
block.dot_into(&query, &mut got);
for (i, v) in vecs.iter().enumerate() {
let want = dot_ref(&query, v);
assert!((got[i] - want).abs() < 1e-4, "lane {i}");
}
}
#[test]
fn partial_block_ignores_padding_lanes() {
let dim = 64;
let vecs = make(5, dim);
let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
let block = PdxBlock::from_rows(&refs, dim);
assert_eq!(block.len(), 5);
let query = vec![0.5f32; dim];
let mut got = vec![f32::NAN; 5];
block.euclidean_sq_into(&query, &mut got);
for (i, v) in vecs.iter().enumerate() {
assert!((got[i] - euclidean_sq_ref(&query, v)).abs() < 1e-4);
}
}
#[test]
fn index_matches_reference_across_block_boundaries() {
let dim = 129; let vecs = make(50, dim);
let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
let index = PdxIndex::from_rows(&refs);
assert_eq!(index.len(), 50);
assert_eq!(index.block_count(), 4);
let query: Vec<f32> = (0..dim).map(|d| (d % 11) as f32 / 11.0).collect();
let got = index.euclidean_sq(&query);
assert_eq!(got.len(), 50);
for (i, v) in vecs.iter().enumerate() {
let want = euclidean_sq_ref(&query, v);
assert!(
(got[i] - want).abs() < 1e-3,
"vector {i}: got {}, want {want}",
got[i]
);
}
}
#[test]
fn single_vector_index_works() {
let dim = 32;
let v: Vec<f32> = (0..dim).map(|d| d as f32).collect();
let index = PdxIndex::from_rows(&[v.as_slice()]);
let got = index.euclidean_sq(&v);
assert_eq!(got.len(), 1);
assert!(got[0].abs() < 1e-5, "distance to self must be zero");
}
#[test]
#[should_panic(expected = "expected 4")]
fn ragged_block_is_rejected() {
let a = [1.0f32, 2.0, 3.0, 4.0];
let b = [1.0f32, 2.0];
PdxBlock::from_rows(&[&a[..], &b[..]], 4);
}
#[test]
#[should_panic(expected = "at most")]
fn oversized_block_is_rejected() {
let v = vec![0.0f32; 8];
let refs: Vec<&[f32]> = (0..LANES + 1).map(|_| v.as_slice()).collect();
PdxBlock::from_rows(&refs, 8);
}
#[test]
#[should_panic(expected = "output buffer too small")]
fn undersized_output_is_rejected() {
let dim = 16;
let vecs = make(20, dim);
let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
let index = PdxIndex::from_rows(&refs);
let mut out = vec![0.0; 5];
index.euclidean_sq_into(&vec![0.0; dim], &mut out);
}
}