1use crate::traits::CompressInto;
7use crate::views::{ChunkOffsetsBase, ChunkOffsetsView};
8use diskann_utils::views::{
9 DenseData,
10 rowmajor::{self, Matrix},
11};
12use diskann_vector::{PureDistanceFunction, distance::SquaredL2};
13use thiserror::Error;
14
15#[derive(Debug, Clone)]
30pub struct BasicTableBase<T, U>
31where
32 T: Matrix<Element = f32>,
33 U: DenseData<Elem = usize>,
34{
35 pivots: T,
36 offsets: ChunkOffsetsBase<U>,
37}
38
39pub type BasicTable = BasicTableBase<rowmajor::Owned<f32>, Box<[usize]>>;
41
42pub type BasicTableView<'a> = BasicTableBase<rowmajor::Ref<'a, f32>, &'a [usize]>;
45
46#[derive(Error, Debug)]
47#[non_exhaustive]
48pub enum BasicTableError {
49 #[error("pivots have {pivot_dim} dimensions while the offsets expect {offsets_dim}")]
50 DimMismatch {
51 pivot_dim: usize,
52 offsets_dim: usize,
53 },
54 #[error("pivots cannot be empty")]
55 PivotsEmpty,
56}
57
58impl<T, U> BasicTableBase<T, U>
59where
60 T: Matrix<Element = f32>,
61 U: DenseData<Elem = usize>,
62{
63 pub fn new(pivots: T, offsets: ChunkOffsetsBase<U>) -> Result<Self, BasicTableError> {
69 let pivot_dim = pivots.ncols();
70 let offsets_dim = offsets.dim();
71
72 if pivot_dim != offsets_dim {
73 Err(BasicTableError::DimMismatch {
74 pivot_dim,
75 offsets_dim,
76 })
77 } else if pivots.nrows() == 0 {
78 Err(BasicTableError::PivotsEmpty)
79 } else {
80 Ok(Self { pivots, offsets })
81 }
82 }
83
84 pub fn view_pivots(&self) -> rowmajor::Ref<'_, f32> {
86 self.pivots.as_view()
87 }
88
89 pub fn view_offsets(&self) -> ChunkOffsetsView<'_> {
91 self.offsets.as_view()
92 }
93
94 pub fn ncenters(&self) -> usize {
96 self.pivots.nrows()
97 }
98
99 pub fn nchunks(&self) -> usize {
101 self.offsets.len()
102 }
103
104 pub fn dim(&self) -> usize {
106 self.pivots.ncols()
107 }
108}
109
110#[derive(Error, Debug)]
111#[non_exhaustive]
112pub enum TableCompressionError {
113 #[error("num centers ({0}) must be at most 256 to compress into a byte vector")]
114 CannotCompressToByte(usize),
115 #[error("invalid input len - expected {0}, got {1}")]
116 InvalidInputDim(usize, usize),
117 #[error("invalid PQ buffer len - expected {0}, got {1}")]
118 InvalidOutputDim(usize, usize),
119 #[error("a value of infinity or NaN was observed while compressing chunk {0}")]
120 InfinityOrNaN(usize),
121}
122
123impl<T, U> CompressInto<&[f32], &mut [u8]> for BasicTableBase<T, U>
124where
125 T: Matrix<Element = f32>,
126 U: DenseData<Elem = usize>,
127{
128 type Error = TableCompressionError;
129 type Output = ();
130
131 fn compress_into(&self, from: &[f32], to: &mut [u8]) -> Result<(), Self::Error> {
162 if self.ncenters() > 256 {
163 return Err(Self::Error::CannotCompressToByte(self.ncenters()));
164 }
165 if from.len() != self.dim() {
166 return Err(Self::Error::InvalidInputDim(self.dim(), from.len()));
167 }
168 if to.len() != self.nchunks() {
169 return Err(Self::Error::InvalidOutputDim(self.nchunks(), to.len()));
170 }
171
172 to.iter_mut().enumerate().try_for_each(|(chunk, to)| {
173 let mut min_distance = f32::INFINITY;
174 let mut min_index = usize::MAX;
175 let range = self.offsets.at(chunk);
176 let slice = &from[range.clone()];
177
178 self.pivots.rows().enumerate().for_each(|(index, row)| {
179 let distance: f32 = SquaredL2::evaluate(slice, &row[range.clone()]);
180 if distance < min_distance {
181 min_distance = distance;
182 min_index = index;
183 }
184 });
185
186 if min_distance.is_infinite() {
187 Err(Self::Error::InfinityOrNaN(chunk))
188 } else {
189 *to = min_index as u8;
191 Ok(())
192 }
193 })
194 }
195}
196
197#[cfg(test)]
202mod tests {
203 use diskann_utils::lazy_format;
204 use rand::{
205 SeedableRng,
206 distr::{Distribution, StandardUniform},
207 };
208
209 use super::*;
210 use crate::product::tables::test::{
211 check_pqtable_single_compression_errors, create_dataset, create_pivot_tables,
212 };
213
214 #[test]
221 fn error_on_mismatch_dim() {
222 let pivots = rowmajor::Owned::from_element(3, 5, 0.0);
223 let offsets = crate::views::ChunkOffsets::new(Box::new([0, 1, 6])).unwrap();
224 let result = BasicTable::new(pivots, offsets);
225 assert!(result.is_err(), "dimensions are not equal");
226 assert_eq!(
227 result.unwrap_err().to_string(),
228 "pivots have 5 dimensions while the offsets expect 6"
229 );
230 }
231
232 #[test]
234 fn error_on_no_pivots() {
235 let pivots = rowmajor::Owned::from_element(0, 5, 0.0);
236 let offsets = crate::views::ChunkOffsets::new(Box::new([0, 1, 2, 5])).unwrap();
237 let result = BasicTable::new(pivots, offsets);
238 assert!(result.is_err(), "pivots is empty");
239 assert_eq!(result.unwrap_err().to_string(), "pivots cannot be empty",);
240 }
241
242 #[test]
243 fn basic_table() {
244 let mut rng = rand::rngs::StdRng::seed_from_u64(0xd96bac968083ec29);
245 for dim in [5, 10, 12] {
246 for total in [1, 2, 3] {
247 let pivots = rowmajor::Owned::from_fn(total, dim, |_| -> f32 {
248 StandardUniform {}.sample(&mut rng)
249 });
250 let offsets = crate::views::ChunkOffsets::new(Box::new([0, 1, 3, dim])).unwrap();
251
252 let table = BasicTable::new(pivots.clone(), offsets.clone()).unwrap();
253
254 assert_eq!(table.ncenters(), total);
255 assert_eq!(table.nchunks(), offsets.len());
256 assert_eq!(table.dim(), offsets.dim());
257 assert_eq!(table.view_pivots().as_view(), pivots.as_view());
258 assert_eq!(table.view_offsets().as_view(), offsets.as_view());
259 }
260 }
261 }
262
263 #[test]
268 fn test_happy_path() {
269 let offsets: Vec<usize> = if cfg!(miri) {
273 vec![0, 1, 3, 6, 10, 15, 21, 28, 36]
274 } else {
275 vec![
276 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91, 105, 120, 136,
277 ]
278 };
279
280 let schema = crate::views::ChunkOffsetsView::new(&offsets).unwrap();
281 let mut rng = rand::rngs::StdRng::seed_from_u64(0xda5b2e661eabacea);
282
283 let num_data = 20;
284 let num_trials = if cfg!(miri) { 1 } else { 10 };
285
286 for &num_centers in [16, 24, 13, 17].iter() {
287 for trial in 0..num_trials {
288 let context = lazy_format!(
289 "happy path, num centers = {}, num data = {}, trial = {}",
290 num_centers,
291 num_data,
292 trial,
293 );
294
295 println!("Currently = {}", context);
296
297 let (pivots, offsets) = create_pivot_tables(schema.to_owned(), num_centers);
298 let table = BasicTable::new(pivots, offsets).unwrap();
299 let (data, expected) = create_dataset(schema, num_centers, num_data, &mut rng);
300
301 let mut output = vec![0; schema.len()];
302 for (input, expected) in std::iter::zip(data.rows(), expected.rows()) {
303 table.compress_into(input, &mut output).unwrap();
304 for (entry, (e, o)) in
305 std::iter::zip(expected.iter(), output.iter()).enumerate()
306 {
307 let o: usize = (*o).into();
308 assert_eq!(*e, o, "unexpected assignment at dim {}", entry);
309 }
310 }
311 }
312 }
313 }
314
315 #[test]
316 fn test_compression_error() {
317 let dim = 10;
318 let num_chunks = 3;
319 let offsets = crate::views::ChunkOffsets::new(Box::new([0, 4, 9, 10])).unwrap();
320
321 {
323 let pivots = rowmajor::Owned::from_element(257, dim, 0.0);
324 let table = BasicTable::new(pivots, offsets.clone()).unwrap();
325
326 let input = vec![f32::default(); dim];
327 let mut output = vec![u8::MAX; num_chunks];
328 let result = table.compress_into(&input, &mut output);
329 assert!(result.is_err());
330 assert_eq!(
331 result.unwrap_err().to_string(),
332 "num centers (257) must be at most 256 to compress into a byte vector"
333 );
334 assert!(
335 output.iter().all(|i| *i == u8::MAX),
336 "output vector should be unmodified"
337 );
338 }
339
340 {
342 let pivots = rowmajor::Owned::from_element(10, dim, 0.0);
343 let table = BasicTable::new(pivots, offsets.clone()).unwrap();
344
345 let input = vec![f32::default(); dim - 1];
346 let mut output = vec![u8::MAX; num_chunks];
347 let result = table.compress_into(&input, &mut output);
348 assert!(result.is_err());
349 assert_eq!(
350 result.unwrap_err().to_string(),
351 format!("invalid input len - expected {}, got {}", dim, dim - 1),
352 );
353 assert!(
354 output.iter().all(|i| *i == u8::MAX),
355 "output vector should be unmodified"
356 );
357 }
358
359 {
361 let pivots = rowmajor::Owned::from_element(10, dim, 0.0);
362 let table = BasicTable::new(pivots, offsets.clone()).unwrap();
363
364 let input = vec![f32::default(); dim];
365 let mut output = vec![u8::MAX; num_chunks - 1];
366 let result = table.compress_into(&input, &mut output);
367 assert!(result.is_err());
368 assert_eq!(
369 result.unwrap_err().to_string(),
370 format!(
371 "invalid PQ buffer len - expected {}, got {}",
372 num_chunks,
373 num_chunks - 1
374 ),
375 );
376 assert!(
377 output.iter().all(|i| *i == u8::MAX),
378 "output vector should be unmodified"
379 );
380 }
381 }
382
383 #[test]
384 fn test_table_single_compression_errors() {
385 check_pqtable_single_compression_errors(
386 &|pivots: rowmajor::Owned<f32>, offsets| BasicTable::new(pivots, offsets).unwrap(),
387 &"BasicTable",
388 )
389 }
390}