1use crate::traits::CompressInto;
7use crate::views::{ChunkOffsetsBase, ChunkOffsetsView};
8use diskann_utils::{
9 strided::Strided,
10 views::{
11 DenseData,
12 rowmajor::{self, Matrix},
13 },
14};
15use diskann_vector::{PureDistanceFunction, distance::SquaredL2};
16use thiserror::Error;
17
18#[derive(Debug, Clone)]
33pub struct BasicTableBase<T, U>
34where
35 T: Matrix<Element = f32>,
36 U: DenseData<Elem = usize>,
37{
38 pivots: T,
39 offsets: ChunkOffsetsBase<U>,
40}
41
42pub type BasicTable = BasicTableBase<rowmajor::Owned<f32>, Box<[usize]>>;
44
45pub type BasicTableView<'a> = BasicTableBase<rowmajor::Ref<'a, f32>, &'a [usize]>;
48
49#[derive(Error, Debug)]
50#[non_exhaustive]
51pub enum BasicTableError {
52 #[error("pivots have {pivot_dim} dimensions while the offsets expect {offsets_dim}")]
53 DimMismatch {
54 pivot_dim: usize,
55 offsets_dim: usize,
56 },
57 #[error("pivots cannot be empty")]
58 PivotsEmpty,
59}
60
61impl<T, U> BasicTableBase<T, U>
62where
63 T: Matrix<Element = f32>,
64 U: DenseData<Elem = usize>,
65{
66 pub fn new(pivots: T, offsets: ChunkOffsetsBase<U>) -> Result<Self, BasicTableError> {
72 let pivot_dim = pivots.ncols();
73 let offsets_dim = offsets.dim();
74
75 if pivot_dim != offsets_dim {
76 Err(BasicTableError::DimMismatch {
77 pivot_dim,
78 offsets_dim,
79 })
80 } else if pivots.nrows() == 0 {
81 Err(BasicTableError::PivotsEmpty)
82 } else {
83 Ok(Self { pivots, offsets })
84 }
85 }
86
87 pub fn view_pivots(&self) -> rowmajor::Ref<'_, f32> {
89 self.pivots.as_view()
90 }
91
92 pub fn view_offsets(&self) -> ChunkOffsetsView<'_> {
94 self.offsets.as_view()
95 }
96
97 pub fn ncenters(&self) -> usize {
99 self.pivots.nrows()
100 }
101
102 pub fn nchunks(&self) -> usize {
104 self.offsets.len()
105 }
106
107 pub fn dim(&self) -> usize {
109 self.pivots.ncols()
110 }
111
112 pub fn as_view(&self) -> BasicTableView<'_> {
114 BasicTableView {
115 pivots: self.pivots.as_view(),
116 offsets: self.offsets.as_view(),
117 }
118 }
119
120 pub fn pivots_for(&self, chunk: usize) -> Option<Strided<'_, f32>> {
124 let range = self.offsets.get(chunk)?;
125
126 #[expect(
127 clippy::expect_used,
128 reason = "the BasicTable's invariants mean this panic should be unreachable"
129 )]
130 let strided = Strided::try_from_data(
131 &self.pivots.as_slice()[range.start..],
132 self.pivots.nrows(),
133 range.len(),
134 self.pivots.ncols(),
135 )
136 .expect("BasicTable asserts that this layout is valid");
137
138 Some(strided)
139 }
140}
141
142#[derive(Error, Debug)]
143#[non_exhaustive]
144pub enum TableCompressionError {
145 #[error("num centers ({0}) must be at most 256 to compress into a byte vector")]
146 CannotCompressToByte(usize),
147 #[error("invalid input len - expected {0}, got {1}")]
148 InvalidInputDim(usize, usize),
149 #[error("invalid PQ buffer len - expected {0}, got {1}")]
150 InvalidOutputDim(usize, usize),
151 #[error("a value of infinity or NaN was observed while compressing chunk {0}")]
152 InfinityOrNaN(usize),
153}
154
155impl<T, U> CompressInto<&[f32], &mut [u8]> for BasicTableBase<T, U>
156where
157 T: Matrix<Element = f32>,
158 U: DenseData<Elem = usize>,
159{
160 type Error = TableCompressionError;
161 type Output = ();
162
163 fn compress_into(&self, from: &[f32], to: &mut [u8]) -> Result<(), Self::Error> {
194 if self.ncenters() > 256 {
195 return Err(Self::Error::CannotCompressToByte(self.ncenters()));
196 }
197 if from.len() != self.dim() {
198 return Err(Self::Error::InvalidInputDim(self.dim(), from.len()));
199 }
200 if to.len() != self.nchunks() {
201 return Err(Self::Error::InvalidOutputDim(self.nchunks(), to.len()));
202 }
203
204 to.iter_mut().enumerate().try_for_each(|(chunk, to)| {
205 let mut min_distance = f32::INFINITY;
206 let mut min_index = usize::MAX;
207 let range = self.offsets.at(chunk);
208 let slice = &from[range.clone()];
209
210 self.pivots.rows().enumerate().for_each(|(index, row)| {
211 let distance: f32 = SquaredL2::evaluate(slice, &row[range.clone()]);
212 if distance < min_distance {
213 min_distance = distance;
214 min_index = index;
215 }
216 });
217
218 if min_distance.is_infinite() {
219 Err(Self::Error::InfinityOrNaN(chunk))
220 } else {
221 *to = min_index as u8;
223 Ok(())
224 }
225 })
226 }
227}
228
229#[cfg(test)]
234mod tests {
235 use diskann_utils::lazy_format;
236 use rand::{
237 SeedableRng,
238 distr::{Distribution, StandardUniform},
239 };
240
241 use super::*;
242 use crate::product::tables::test::{
243 check_pqtable_single_compression_errors, create_dataset, create_pivot_tables,
244 };
245
246 #[test]
253 fn error_on_mismatch_dim() {
254 let pivots = rowmajor::Owned::from_element(3, 5, 0.0);
255 let offsets = crate::views::ChunkOffsets::new(Box::new([0, 1, 6])).unwrap();
256 let result = BasicTable::new(pivots, offsets);
257 assert!(result.is_err(), "dimensions are not equal");
258 assert_eq!(
259 result.unwrap_err().to_string(),
260 "pivots have 5 dimensions while the offsets expect 6"
261 );
262 }
263
264 #[test]
266 fn error_on_no_pivots() {
267 let pivots = rowmajor::Owned::from_element(0, 5, 0.0);
268 let offsets = crate::views::ChunkOffsets::new(Box::new([0, 1, 2, 5])).unwrap();
269 let result = BasicTable::new(pivots, offsets);
270 assert!(result.is_err(), "pivots is empty");
271 assert_eq!(result.unwrap_err().to_string(), "pivots cannot be empty",);
272 }
273
274 #[test]
275 fn basic_table() {
276 let mut rng = rand::rngs::StdRng::seed_from_u64(0xd96bac968083ec29);
277 for dim in [5, 10, 12] {
278 for total in [1, 2, 3] {
279 let pivots = rowmajor::Owned::from_fn(total, dim, |_| -> f32 {
280 StandardUniform {}.sample(&mut rng)
281 });
282 let offsets = crate::views::ChunkOffsets::new(Box::new([0, 1, 3, dim])).unwrap();
283
284 let table = BasicTable::new(pivots.clone(), offsets.clone()).unwrap();
285
286 assert_eq!(table.ncenters(), total);
287 assert_eq!(table.nchunks(), offsets.len());
288 assert_eq!(table.dim(), offsets.dim());
289 assert_eq!(table.view_pivots().as_view(), pivots.as_view());
290 assert_eq!(table.view_offsets().as_view(), offsets.as_view());
291 }
292 }
293 }
294
295 #[test]
300 fn test_happy_path() {
301 let offsets: Vec<usize> = if cfg!(miri) {
305 vec![0, 1, 3, 6, 10, 15, 21, 28, 36]
306 } else {
307 vec![
308 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91, 105, 120, 136,
309 ]
310 };
311
312 let schema = crate::views::ChunkOffsetsView::new(&offsets).unwrap();
313 let mut rng = rand::rngs::StdRng::seed_from_u64(0xda5b2e661eabacea);
314
315 let num_data = 20;
316 let num_trials = if cfg!(miri) { 1 } else { 10 };
317
318 for &num_centers in [16, 24, 13, 17].iter() {
319 for trial in 0..num_trials {
320 let context = lazy_format!(
321 "happy path, num centers = {}, num data = {}, trial = {}",
322 num_centers,
323 num_data,
324 trial,
325 );
326
327 println!("Currently = {}", context);
328
329 let (pivots, offsets) = create_pivot_tables(schema.to_owned(), num_centers);
330 let table = BasicTable::new(pivots, offsets).unwrap();
331
332 for chunk in 0..schema.len() {
335 let strided = table.pivots_for(chunk).unwrap();
336 assert_eq!(strided.nrows(), num_centers);
337 assert_eq!(strided.ncols(), schema.at(chunk).len());
338
339 for (center, row) in strided.rows().enumerate() {
340 let base = ((center + chunk) % num_centers) as f32;
341 row.iter().enumerate().for_each(|(dim, b)| {
342 let offset = if dim.is_multiple_of(2) { 0.25 } else { -0.25 };
343 assert_eq!(
344 *b,
345 base + offset,
346 "failed: chunk {} of {}, center {} of {}, dim {} of {}",
347 chunk,
348 schema.len(),
349 center,
350 num_centers,
351 dim,
352 row.len(),
353 );
354 })
355 }
356 }
357
358 assert!(
359 table.pivots_for(schema.len()).is_none(),
360 "out of bounds access should return `None`",
361 );
362
363 let (data, expected) = create_dataset(schema, num_centers, num_data, &mut rng);
364
365 let mut output = vec![0; schema.len()];
366 for (input, expected) in std::iter::zip(data.rows(), expected.rows()) {
367 table.compress_into(input, &mut output).unwrap();
368 for (entry, (e, o)) in
369 std::iter::zip(expected.iter(), output.iter()).enumerate()
370 {
371 let o: usize = (*o).into();
372 assert_eq!(*e, o, "unexpected assignment at dim {}", entry);
373 }
374 }
375 }
376 }
377 }
378
379 #[test]
380 fn test_compression_error() {
381 let dim = 10;
382 let num_chunks = 3;
383 let offsets = crate::views::ChunkOffsets::new(Box::new([0, 4, 9, 10])).unwrap();
384
385 {
387 let pivots = rowmajor::Owned::from_element(257, dim, 0.0);
388 let table = BasicTable::new(pivots, offsets.clone()).unwrap();
389
390 let input = vec![f32::default(); dim];
391 let mut output = vec![u8::MAX; num_chunks];
392 let result = table.compress_into(&input, &mut output);
393 assert!(result.is_err());
394 assert_eq!(
395 result.unwrap_err().to_string(),
396 "num centers (257) must be at most 256 to compress into a byte vector"
397 );
398 assert!(
399 output.iter().all(|i| *i == u8::MAX),
400 "output vector should be unmodified"
401 );
402 }
403
404 {
406 let pivots = rowmajor::Owned::from_element(10, dim, 0.0);
407 let table = BasicTable::new(pivots, offsets.clone()).unwrap();
408
409 let input = vec![f32::default(); dim - 1];
410 let mut output = vec![u8::MAX; num_chunks];
411 let result = table.compress_into(&input, &mut output);
412 assert!(result.is_err());
413 assert_eq!(
414 result.unwrap_err().to_string(),
415 format!("invalid input len - expected {}, got {}", dim, dim - 1),
416 );
417 assert!(
418 output.iter().all(|i| *i == u8::MAX),
419 "output vector should be unmodified"
420 );
421 }
422
423 {
425 let pivots = rowmajor::Owned::from_element(10, dim, 0.0);
426 let table = BasicTable::new(pivots, offsets.clone()).unwrap();
427
428 let input = vec![f32::default(); dim];
429 let mut output = vec![u8::MAX; num_chunks - 1];
430 let result = table.compress_into(&input, &mut output);
431 assert!(result.is_err());
432 assert_eq!(
433 result.unwrap_err().to_string(),
434 format!(
435 "invalid PQ buffer len - expected {}, got {}",
436 num_chunks,
437 num_chunks - 1
438 ),
439 );
440 assert!(
441 output.iter().all(|i| *i == u8::MAX),
442 "output vector should be unmodified"
443 );
444 }
445 }
446
447 #[test]
448 fn test_table_single_compression_errors() {
449 check_pqtable_single_compression_errors(
450 &|pivots: rowmajor::Owned<f32>, offsets| BasicTable::new(pivots, offsets).unwrap(),
451 &"BasicTable",
452 )
453 }
454}