1use std::fmt;
54
55pub const LANES: usize = 16;
58
59#[derive(Clone, PartialEq)]
61pub struct PdxBlock {
62 data: Vec<f32>,
64 dim: usize,
65 len: usize,
67}
68
69impl fmt::Debug for PdxBlock {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.debug_struct("PdxBlock")
72 .field("dim", &self.dim)
73 .field("len", &self.len)
74 .finish_non_exhaustive()
75 }
76}
77
78impl PdxBlock {
79 pub fn from_rows(vectors: &[&[f32]], dim: usize) -> Self {
86 assert!(
87 vectors.len() <= LANES,
88 "block holds at most {LANES} vectors, got {}",
89 vectors.len()
90 );
91 for (i, v) in vectors.iter().enumerate() {
92 assert_eq!(
93 v.len(),
94 dim,
95 "vector {i} has length {}, expected {dim}",
96 v.len()
97 );
98 }
99
100 let mut data = vec![0.0f32; dim * LANES];
101 for (lane, v) in vectors.iter().enumerate() {
102 for (d, &value) in v.iter().enumerate() {
103 data[d * LANES + lane] = value;
104 }
105 }
106 Self {
107 data,
108 dim,
109 len: vectors.len(),
110 }
111 }
112
113 pub fn dim(&self) -> usize {
114 self.dim
115 }
116
117 pub fn len(&self) -> usize {
118 self.len
119 }
120
121 pub fn is_empty(&self) -> bool {
122 self.len == 0
123 }
124
125 pub fn euclidean_sq_into(&self, query: &[f32], out: &mut [f32]) {
133 assert_eq!(query.len(), self.dim, "query dimension mismatch");
134 assert!(out.len() >= self.len, "output buffer too small");
135
136 let mut acc = [0.0f32; LANES];
137
138 for (d, &q) in query.iter().enumerate() {
139 let row = &self.data[d * LANES..d * LANES + LANES];
140 for lane in 0..LANES {
143 let diff = row[lane] - q;
144 acc[lane] = diff.mul_add(diff, acc[lane]);
145 }
146 }
147
148 out[..self.len].copy_from_slice(&acc[..self.len]);
149 }
150
151 pub fn dot_into(&self, query: &[f32], out: &mut [f32]) {
153 assert_eq!(query.len(), self.dim, "query dimension mismatch");
154 assert!(out.len() >= self.len, "output buffer too small");
155
156 let mut acc = [0.0f32; LANES];
157
158 for (d, &q) in query.iter().enumerate() {
159 let row = &self.data[d * LANES..d * LANES + LANES];
160 for lane in 0..LANES {
161 acc[lane] = row[lane].mul_add(q, acc[lane]);
162 }
163 }
164
165 out[..self.len].copy_from_slice(&acc[..self.len]);
166 }
167}
168
169#[derive(Debug, Clone)]
171pub struct PdxIndex {
172 blocks: Vec<PdxBlock>,
173 dim: usize,
174 len: usize,
175}
176
177impl PdxIndex {
178 pub fn from_rows(vectors: &[&[f32]]) -> Self {
183 assert!(
184 !vectors.is_empty(),
185 "cannot build a PdxIndex from no vectors"
186 );
187 let dim = vectors[0].len();
188
189 let blocks = vectors
190 .chunks(LANES)
191 .map(|chunk| PdxBlock::from_rows(chunk, dim))
192 .collect();
193
194 Self {
195 blocks,
196 dim,
197 len: vectors.len(),
198 }
199 }
200
201 pub fn dim(&self) -> usize {
202 self.dim
203 }
204
205 pub fn len(&self) -> usize {
206 self.len
207 }
208
209 pub fn is_empty(&self) -> bool {
210 self.len == 0
211 }
212
213 pub fn block_count(&self) -> usize {
214 self.blocks.len()
215 }
216
217 pub fn euclidean_sq_into(&self, query: &[f32], out: &mut [f32]) {
226 assert!(out.len() >= self.len, "output buffer too small");
227 let mut written = 0;
228 for block in &self.blocks {
229 block.euclidean_sq_into(query, &mut out[written..]);
230 written += block.len();
231 }
232 }
233
234 pub fn dot_into(&self, query: &[f32], out: &mut [f32]) {
236 assert!(out.len() >= self.len, "output buffer too small");
237 let mut written = 0;
238 for block in &self.blocks {
239 block.dot_into(query, &mut out[written..]);
240 written += block.len();
241 }
242 }
243
244 pub fn euclidean_sq(&self, query: &[f32]) -> Vec<f32> {
246 let mut out = vec![0.0; self.len];
247 self.euclidean_sq_into(query, &mut out);
248 out
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 fn euclidean_sq_ref(a: &[f32], b: &[f32]) -> f32 {
259 a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
260 }
261
262 fn dot_ref(a: &[f32], b: &[f32]) -> f32 {
263 a.iter().zip(b).map(|(x, y)| x * y).sum()
264 }
265
266 fn make(n: usize, dim: usize) -> Vec<Vec<f32>> {
267 (0..n)
268 .map(|i| {
269 (0..dim)
270 .map(|d| ((i * 31 + d * 17) % 101) as f32 / 101.0)
271 .collect()
272 })
273 .collect()
274 }
275
276 #[test]
277 fn block_matches_reference_euclidean() {
278 let dim = 128;
279 let vecs = make(LANES, dim);
280 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
281 let block = PdxBlock::from_rows(&refs, dim);
282 let query: Vec<f32> = (0..dim).map(|d| (d % 7) as f32 / 7.0).collect();
283
284 let mut got = vec![0.0; LANES];
285 block.euclidean_sq_into(&query, &mut got);
286
287 for (i, v) in vecs.iter().enumerate() {
288 let want = euclidean_sq_ref(&query, v);
289 assert!(
290 (got[i] - want).abs() < 1e-4,
291 "lane {i}: got {}, want {want}",
292 got[i]
293 );
294 }
295 }
296
297 #[test]
298 fn block_matches_reference_dot() {
299 let dim = 96;
300 let vecs = make(LANES, dim);
301 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
302 let block = PdxBlock::from_rows(&refs, dim);
303 let query: Vec<f32> = (0..dim).map(|d| (d % 5) as f32 / 5.0).collect();
304
305 let mut got = vec![0.0; LANES];
306 block.dot_into(&query, &mut got);
307
308 for (i, v) in vecs.iter().enumerate() {
309 let want = dot_ref(&query, v);
310 assert!((got[i] - want).abs() < 1e-4, "lane {i}");
311 }
312 }
313
314 #[test]
315 fn partial_block_ignores_padding_lanes() {
316 let dim = 64;
319 let vecs = make(5, dim);
320 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
321 let block = PdxBlock::from_rows(&refs, dim);
322 assert_eq!(block.len(), 5);
323
324 let query = vec![0.5f32; dim];
325 let mut got = vec![f32::NAN; 5];
326 block.euclidean_sq_into(&query, &mut got);
327
328 for (i, v) in vecs.iter().enumerate() {
329 assert!((got[i] - euclidean_sq_ref(&query, v)).abs() < 1e-4);
330 }
331 }
332
333 #[test]
334 fn index_matches_reference_across_block_boundaries() {
335 let dim = 129; let vecs = make(50, dim);
339 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
340 let index = PdxIndex::from_rows(&refs);
341 assert_eq!(index.len(), 50);
342 assert_eq!(index.block_count(), 4);
343
344 let query: Vec<f32> = (0..dim).map(|d| (d % 11) as f32 / 11.0).collect();
345 let got = index.euclidean_sq(&query);
346
347 assert_eq!(got.len(), 50);
348 for (i, v) in vecs.iter().enumerate() {
349 let want = euclidean_sq_ref(&query, v);
350 assert!(
351 (got[i] - want).abs() < 1e-3,
352 "vector {i}: got {}, want {want}",
353 got[i]
354 );
355 }
356 }
357
358 #[test]
359 fn single_vector_index_works() {
360 let dim = 32;
361 let v: Vec<f32> = (0..dim).map(|d| d as f32).collect();
362 let index = PdxIndex::from_rows(&[v.as_slice()]);
363 let got = index.euclidean_sq(&v);
364 assert_eq!(got.len(), 1);
365 assert!(got[0].abs() < 1e-5, "distance to self must be zero");
366 }
367
368 #[test]
369 #[should_panic(expected = "expected 4")]
370 fn ragged_block_is_rejected() {
371 let a = [1.0f32, 2.0, 3.0, 4.0];
372 let b = [1.0f32, 2.0];
373 PdxBlock::from_rows(&[&a[..], &b[..]], 4);
374 }
375
376 #[test]
377 #[should_panic(expected = "at most")]
378 fn oversized_block_is_rejected() {
379 let v = vec![0.0f32; 8];
380 let refs: Vec<&[f32]> = (0..LANES + 1).map(|_| v.as_slice()).collect();
381 PdxBlock::from_rows(&refs, 8);
382 }
383
384 #[test]
385 #[should_panic(expected = "output buffer too small")]
386 fn undersized_output_is_rejected() {
387 let dim = 16;
388 let vecs = make(20, dim);
389 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
390 let index = PdxIndex::from_rows(&refs);
391 let mut out = vec![0.0; 5];
392 index.euclidean_sq_into(&vec![0.0; dim], &mut out);
393 }
394}