1use std::num::NonZeroUsize;
7
8use super::pivots;
9use crate::{
10 product::tables::basic::TableCompressionError,
11 traits::CompressInto,
12 views::{ChunkOffsets, ChunkOffsetsView},
13};
14use diskann_utils::{
15 strided::Strided,
16 views::{
17 self,
18 rowmajor::{self, Matrix, MatrixMut},
19 },
20};
21use thiserror::Error;
22
23#[derive(Debug)]
39pub struct TransposedTable {
40 pivots: Box<[pivots::Chunk]>,
41 offsets: crate::views::ChunkOffsets,
42 largest: usize,
44 ncenters: usize,
46}
47
48#[derive(Debug, Error)]
49#[non_exhaustive]
50pub enum TransposedTableError {
51 #[error("pivots have {pivot_dim} dimensions while the offsets expect {offsets_dim}")]
52 DimMismatch {
53 pivot_dim: usize,
54 offsets_dim: usize,
55 },
56 #[error("error constructing pivot {problem} of {total}")]
57 PivotError {
58 problem: usize,
59 total: usize,
60 source: pivots::ChunkConstructionError,
61 },
62}
63
64impl TransposedTable {
65 #[expect(clippy::expect_used)]
76 pub fn from_parts(
77 pivots: views::rowmajor::Ref<f32>,
78 offsets: ChunkOffsets,
79 ) -> Result<Self, TransposedTableError> {
80 let pivot_dim = pivots.ncols();
81 let offsets_dim = offsets.dim();
82 if pivot_dim != offsets_dim {
83 return Err(TransposedTableError::DimMismatch {
84 pivot_dim,
85 offsets_dim,
86 });
87 }
88
89 let ncenters = pivots.nrows();
90 let mut largest = 0;
91 let pivots: Box<[_]> = (0..offsets.len())
92 .map(|i| {
93 let range = offsets.at(i);
94 largest = largest.max(range.len());
95 let view = Strided::try_from_data(
96 &(pivots.as_slice()[range.start..]),
97 pivots.nrows(),
98 range.len(),
99 offsets.dim(),
100 )
101 .expect(
102 "the check on `pivot_dim` and `offsets_dim` should cause this to never error",
103 );
104 pivots::Chunk::new(view).map_err(|source| TransposedTableError::PivotError {
105 problem: i,
106 total: offsets.len(),
107 source,
108 })
109 })
110 .collect::<Result<Box<[_]>, TransposedTableError>>()?;
111
112 debug_assert_eq!(pivots.len(), offsets.len());
113 Ok(Self {
114 pivots,
115 offsets,
116 largest,
117 ncenters,
118 })
119 }
120
121 pub fn ncenters(&self) -> usize {
123 self.ncenters
124 }
125
126 pub fn nchunks(&self) -> usize {
128 self.offsets.len()
129 }
130
131 pub fn dim(&self) -> usize {
133 self.offsets.dim()
134 }
135
136 pub fn view_offsets(&self) -> ChunkOffsetsView<'_> {
138 self.offsets.as_view()
139 }
140
141 #[expect(clippy::expect_used)]
158 pub fn compress_batch<T, F, DelegateError>(
159 &self,
160 data: views::rowmajor::Ref<'_, T>,
161 mut compression_delegate: F,
162 ) -> Result<(), CompressError<DelegateError>>
163 where
164 T: Copy + Into<f32>,
165 F: FnMut(RowChunk, pivots::CompressionResult) -> Result<(), DelegateError>,
166 DelegateError: std::error::Error,
167 {
168 assert_eq!(
169 data.ncols(),
170 self.dim(),
171 "schema expects {} dimensions but data has {}",
172 self.dim(),
173 data.ncols()
174 );
175
176 const SUB_BATCH_SIZE: usize = pivots::Chunk::batchsize();
178
179 let dim_nonzero = self.offsets.dim_nonzero();
180 let mut packing_buffer: Box<[f32]> =
181 (0..self.largest * SUB_BATCH_SIZE).map(|_| 0.0).collect();
182
183 let nrows = data.nrows();
186 let ncols = data.ncols();
187 let slice = data.as_slice();
188
189 for (i, chunk) in self.pivots.iter().enumerate() {
190 let range = self.offsets.at(i);
191 if let Some(chunk_dim) = NonZeroUsize::new(range.len()) {
192 let mut packing_view = views::rowmajor::Mut::try_from_data(
194 &mut packing_buffer[..SUB_BATCH_SIZE * chunk_dim.get()],
195 SUB_BATCH_SIZE,
196 chunk_dim.get(),
197 )
198 .expect("the packing buffer should have been sized correctly");
199
200 for row_start in (0..nrows).step_by(SUB_BATCH_SIZE) {
201 let row_end = nrows.min(row_start + SUB_BATCH_SIZE);
202
203 if row_end - row_start == SUB_BATCH_SIZE {
205 let mut linear_start = row_start * ncols + range.start;
211 packing_view.rows_mut().for_each(|row| {
212 pack(row, &slice[linear_start..linear_start + chunk_dim.get()]);
213 linear_start += dim_nonzero.get();
214 });
215
216 let result = chunk.find_closest_batch(packing_view.as_view().into());
217
218 for (j, &r) in result.iter().enumerate() {
222 compression_delegate(
223 RowChunk {
224 row: row_start + j,
225 chunk: i,
226 },
227 r,
228 )
229 .map_err(|inner| CompressError {
230 inner,
231 row: row_start + j,
232 chunk: i,
233 nearest: r.into_inner(),
234 })?;
235 }
236 } else {
237 for row in row_start..row_end {
239 let linear_start = row * ncols + range.start;
240 let linear_stop = linear_start + range.len();
241
242 let packed = &mut packing_view.row_mut(0);
244 pack(packed, &slice[linear_start..linear_stop]);
245 let result = chunk.find_closest(packed);
246
247 compression_delegate(RowChunk { row, chunk: i }, result).map_err(
248 |inner| CompressError {
249 inner,
250 row,
251 chunk: i,
252 nearest: result.into_inner(),
253 },
254 )?;
255 }
256 }
257 }
258 }
259 }
260 Ok(())
261 }
262
263 pub fn process_into<T>(&self, query: &[f32], mut partials: rowmajor::Mut<'_, f32>)
289 where
290 T: pivots::ProcessInto,
291 {
292 assert_eq!(
294 query.len(),
295 self.dim(),
296 "query has the wrong number of dimensions"
297 );
298 assert_eq!(
299 partials.ncols(),
300 self.ncenters(),
301 "output has the wrong number of columns"
302 );
303 assert_eq!(
304 partials.nrows(),
305 self.nchunks(),
306 "output has the wrong number of rows"
307 );
308
309 std::iter::zip(self.pivots.iter(), partials.rows_mut())
311 .enumerate()
312 .for_each(|(i, (pivot, out))| {
313 let range = self.offsets.at(i);
314 T::process_into(pivot, &query[range], out);
315 });
316 }
317}
318
319pub struct RowChunk {
322 row: usize,
323 chunk: usize,
324}
325
326#[derive(Error, Debug)]
327#[error(
328 "compression delegate returned \"{inner}\" when processing row {row} and chunk \
329 {chunk} with nearest center {nearest}"
330)]
331pub struct CompressError<DelegateError: std::error::Error> {
332 inner: DelegateError,
333 row: usize,
334 chunk: usize,
335 nearest: u32,
336}
337
338#[inline(always)]
339fn pack<T>(dst: &mut [f32], src: &[T])
340where
341 T: Copy + Into<f32>,
342{
343 debug_assert_eq!(dst.len(), src.len());
344 std::iter::zip(dst.iter_mut(), src.iter()).for_each(|(d, &s)| *d = s.into());
345}
346
347impl CompressInto<&[f32], &mut [u8]> for TransposedTable {
352 type Error = TableCompressionError;
353 type Output = ();
354
355 fn compress_into(&self, from: &[f32], to: &mut [u8]) -> Result<(), Self::Error> {
386 if self.ncenters() > 256 {
387 return Err(Self::Error::CannotCompressToByte(self.ncenters()));
388 }
389 if from.len() != self.dim() {
390 return Err(Self::Error::InvalidInputDim(self.dim(), from.len()));
391 }
392 if to.len() != self.nchunks() {
393 return Err(Self::Error::InvalidOutputDim(self.nchunks(), to.len()));
394 }
395
396 std::iter::zip(self.pivots.iter(), to.iter_mut())
397 .enumerate()
398 .try_for_each(|(i, (pivot, to))| {
399 let range = self.offsets.at(i);
400 let result = pivot.find_closest(&from[range]);
401 result.map(
402 |v| *to = v as u8, || Self::Error::InfinityOrNaN(i),
404 )
405 })
406 }
407}
408
409#[derive(Error, Debug)]
410pub enum TableBatchCompressionError {
411 #[error("num centers ({0}) must be at most 256 to compress into a byte vector")]
412 CannotCompressToByte(usize),
413 #[error("invalid input len - expected {0}, got {1}")]
414 InvalidInputDim(usize, usize),
415 #[error("invalid PQ buffer len - expected {0}, got {1}")]
416 InvalidOutputDim(usize, usize),
417 #[error(
418 "input and output must have the same number of rows - instead, got {0} and {1} \
419 (respectively)"
420 )]
421 UnequalRows(usize, usize),
422 #[error(
423 "a value of infinity or NaN was observed while compressing chunk {0} of batch input {1}"
424 )]
425 InfinityOrNaN(usize, usize),
426}
427
428impl<T> CompressInto<rowmajor::Ref<'_, T>, rowmajor::Mut<'_, u8>> for TransposedTable
429where
430 T: Copy + Into<f32>,
431{
432 type Error = TableBatchCompressionError;
433 type Output = ();
434
435 fn compress_into(
469 &self,
470 from: rowmajor::Ref<'_, T>,
471 mut to: rowmajor::Mut<'_, u8>,
472 ) -> Result<(), Self::Error> {
473 if self.ncenters() > 256 {
474 return Err(Self::Error::CannotCompressToByte(self.ncenters()));
475 }
476 if from.ncols() != self.dim() {
477 return Err(Self::Error::InvalidInputDim(self.dim(), from.ncols()));
478 }
479 if to.ncols() != self.nchunks() {
480 return Err(Self::Error::InvalidOutputDim(self.nchunks(), to.ncols()));
481 }
482 if from.nrows() != to.nrows() {
483 return Err(Self::Error::UnequalRows(from.nrows(), to.nrows()));
484 }
485
486 #[derive(Debug, Error)]
489 #[error("unreachable")]
490 struct PassThrough;
491
492 let result = self.compress_batch(
495 from,
496 |RowChunk { row, chunk }, result| -> Result<(), PassThrough> {
497 result.map(|v| *to.element_mut(row, chunk) = v as u8, || PassThrough)
498 },
499 );
500
501 result.map_err(|err| Self::Error::InfinityOrNaN(err.chunk, err.row))
502 }
503}
504
505#[cfg(test)]
506mod test_compression {
507 use std::collections::HashSet;
508
509 use diskann_vector::{PureDistanceFunction, distance};
510 use rand::{
511 Rng, SeedableRng,
512 distr::{Distribution, StandardUniform, Uniform},
513 rngs::StdRng,
514 };
515
516 use super::*;
517 #[cfg(not(miri))]
518 use crate::product::tables::test::{
519 check_pqtable_batch_compression_errors, check_pqtable_single_compression_errors,
520 };
521 use crate::{
522 distances::{InnerProduct, SquaredL2},
523 error::format,
524 product::tables::test::{create_dataset, create_pivot_tables},
525 };
526 use diskann_utils::lazy_format;
527
528 #[test]
535 fn error_on_mismatch_dim() {
536 let pivots = views::rowmajor::Owned::from_element(3, 5, 0.0);
537 let offsets = ChunkOffsets::new(Box::new([0, 1, 6])).unwrap();
538 let result = TransposedTable::from_parts(pivots.as_view(), offsets);
539 assert!(result.is_err(), "dimensions are not equal");
540 assert_eq!(
541 result.unwrap_err().to_string(),
542 "pivots have 5 dimensions while the offsets expect 6"
543 );
544 }
545
546 #[test]
549 fn error_on_empty() {
550 let pivots = views::rowmajor::Owned::from_element(0, 5, 0.0);
551 let offsets = ChunkOffsets::new(Box::new([0, 1, 5])).unwrap();
552 let result = TransposedTable::from_parts(pivots.as_view(), offsets);
553 assert!(result.is_err(), "dimensions are not equal");
554
555 let expected = [
556 "error constructing pivot 0 of 2",
557 " caused by: cannot construct a Chunk from a source with zero length",
558 ]
559 .join("\n");
560
561 assert_eq!(format(&result.unwrap_err()), expected,);
562 }
563
564 #[test]
565 fn basic_table() {
566 let mut rng = StdRng::seed_from_u64(0xd96bac968083ec29);
567 for dim in [5, 10, 12] {
568 for total in [1, 2, 3, 7, 8, 9, 10] {
570 let pivots = views::rowmajor::Owned::from_fn(total, dim, |_| -> f32 {
571 StandardUniform {}.sample(&mut rng)
572 });
573 let offsets = ChunkOffsets::new(Box::new([0, 1, 3, dim])).unwrap();
574 let table = TransposedTable::from_parts(pivots.as_view(), offsets.clone()).unwrap();
575
576 assert_eq!(table.ncenters(), total);
577 assert_eq!(table.nchunks(), offsets.len());
578 assert_eq!(table.dim(), offsets.dim());
579
580 for chunk in 0..offsets.len() {
583 let range = offsets.at(chunk);
584 let pivot = &table.pivots[chunk];
585 for row in 0..total {
586 let r = &pivots.row(row)[range.clone()];
587 for (col, expected) in r.iter().enumerate() {
588 assert_eq!(pivot.get(row, col), *expected);
589 }
590 }
591 }
592
593 assert_eq!(table.view_offsets(), offsets.as_view());
594 }
595 }
596 }
597
598 #[derive(Error, Debug)]
603 #[error("unreachable reached")]
604 struct Infallible;
605
606 #[test]
607 fn test_happy_path() {
608 let offsets: Vec<usize> = if cfg!(miri) {
612 vec![0, 1, 3, 6, 10, 15, 21, 28, 36]
613 } else {
614 vec![
615 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91, 105, 120, 136,
616 ]
617 };
618
619 let schema = ChunkOffsetsView::new(&offsets).unwrap();
620 let mut rng = StdRng::seed_from_u64(0x88e3d3366501ad6c);
621
622 let num_data = if cfg!(miri) {
623 vec![0, 7, 8, 10]
624 } else {
625 vec![0, 1, 2, 3, 4, 16, 17, 18, 19]
626 };
627
628 let num_trials = if cfg!(miri) { 1 } else { 10 };
629
630 for &num_centers in [16, 24, 13, 17].iter() {
639 for &num_data in num_data.iter() {
640 for trial in 0..num_trials {
641 let context = lazy_format!(
642 "happy path, num centers = {}, num data = {}, trial = {}",
643 num_centers,
644 num_data,
645 trial,
646 );
647
648 println!("Currently = {}", context);
649
650 let (pivots, offsets) = create_pivot_tables(schema.to_owned(), num_centers);
651 let table = TransposedTable::from_parts(pivots.as_view(), offsets).unwrap();
652 let (data, expected) = create_dataset(schema, num_centers, num_data, &mut rng);
653
654 let mut called = HashSet::<(usize, usize)>::new();
655
656 table
658 .compress_batch(
659 data.as_view(),
660 |RowChunk { row, chunk }, value| -> Result<(), Infallible> {
661 assert!(value.is_okay());
662 assert_eq!(
664 value.unwrap() as usize,
665 *expected.element(row, chunk),
666 "failed at (row = {row}, chunk = {chunk}). data = {:?}, context: {}",
667 &(data.row(row)[schema.at(chunk)]),
668 context,
669 );
670
671 assert!(
675 called.insert((row, chunk)),
676 "row {row} and chunk {chunk}, called multiple times. Context = {}",
677 context,
678 );
679
680 Ok(())
681 },
682 )
683 .unwrap();
684
685 assert_eq!(called.len(), num_data * schema.len());
686
687 let mut output =
689 views::rowmajor::Owned::from_element(num_data, schema.len(), 0);
690 table
691 .compress_into(data.as_view(), output.as_view_mut())
692 .unwrap();
693
694 assert_eq!(output.nrows(), expected.nrows());
695 assert_eq!(output.ncols(), expected.ncols());
696 for row in 0..output.nrows() {
697 for col in 0..output.ncols() {
698 assert_eq!(
699 *output.element(row, col) as usize,
700 *expected.element(row, col),
701 "failed on row {}, col {}. Context = {}",
702 row,
703 col,
704 context,
705 );
706 }
707 }
708
709 let mut output = vec![0; schema.len()];
711 for (i, (row, expected)) in
712 std::iter::zip(data.rows(), expected.rows()).enumerate()
713 {
714 table.compress_into(row, output.as_mut_slice()).unwrap();
715 for (d, (o, e)) in
716 std::iter::zip(output.iter(), expected.iter()).enumerate()
717 {
718 assert_eq!(
719 *o as usize, *e,
720 "failed on row {}, col {}. Context = {}",
721 i, d, context
722 );
723 }
724 }
725 }
726 }
727 }
728 }
729
730 #[test]
735 #[should_panic(expected = "schema expects 4 dimensions but data has 5")]
736 fn panic_on_dim_mismatch() {
737 let offsets = [0, 4];
738 let data: Vec<f32> = vec![0.0; 5];
739
740 let schema = ChunkOffsetsView::new(&offsets).unwrap();
741 let (pivots, offsets) = create_pivot_tables(schema.to_owned(), 3);
742 let table = TransposedTable::from_parts(pivots.as_view(), offsets).unwrap();
743
744 let _ = table.compress_batch(
746 views::rowmajor::Ref::try_from_data(data.as_slice(), 1, 5).unwrap(),
747 |_, _| -> Result<(), Infallible> { panic!("this shouldn't be called") },
748 );
749 }
750
751 #[derive(Error, Debug)]
752 #[error("compression delegate error with {0}")]
753 struct DelegateError(u64);
754
755 #[test]
759 fn test_delegate_error_propagation() {
760 let offsets: Vec<usize> = vec![0, 1, 7];
761 let schema = ChunkOffsetsView::new(&offsets).unwrap();
762 let mut rng = StdRng::seed_from_u64(0xc35a90da17fafa2a);
763
764 let num_centers = 3;
765 let num_data = 7;
766
767 let (pivots, offsets) = create_pivot_tables(schema.to_owned(), num_centers);
768 let table = TransposedTable::from_parts(pivots.as_view(), offsets).unwrap();
769 let (data, _) = create_dataset(schema, num_centers, num_data, &mut rng);
770
771 let data_view =
772 views::rowmajor::Ref::try_from_data(data.as_slice(), num_data, schema.dim()).unwrap();
773 let distribution = rand_distr::StandardUniform {};
774
775 for row in 0..data_view.nrows() {
776 for chunk in 0..schema.len() {
777 let context = lazy_format!("row = {row}, chunk = {chunk}");
778
779 let value: u64 = rng.sample(distribution);
781
782 let result = table.compress_batch(
783 data_view,
784 |RowChunk {
785 row: this_row,
786 chunk: this_chunk,
787 },
788 _| {
789 if this_row == row && this_chunk == chunk {
790 Err(DelegateError(value))
791 } else {
792 Ok(())
793 }
794 },
795 );
796 assert!(result.is_err(), "{}", context);
797
798 let message = result.unwrap_err().to_string();
799 assert!(
800 message.contains(&format!("{}", DelegateError(value))),
801 "{}",
802 context
803 );
804 assert!(message.contains("delegate returned"));
805 assert!(message.contains(&format!("when processing row {row} and chunk {chunk}")));
806 }
807 }
808 }
809
810 #[test]
811 #[cfg(not(miri))]
812 fn test_table_single_compression_errors() {
813 check_pqtable_single_compression_errors(
814 &|pivots: views::rowmajor::Owned<f32>, offsets| {
815 TransposedTable::from_parts(pivots.as_view(), offsets).unwrap()
816 },
817 &"TranposedTable",
818 )
819 }
820
821 #[test]
822 #[cfg(not(miri))]
823 fn test_table_batch_compression_errors() {
824 check_pqtable_batch_compression_errors(
825 &|pivots: views::rowmajor::Owned<f32>, offsets| {
826 TransposedTable::from_parts(pivots.as_view(), offsets).unwrap()
827 },
828 &"TranposedTable",
829 )
830 }
831
832 fn test_process_into_impl(
837 num_chunks: usize,
838 num_centers: usize,
839 num_trials: usize,
840 rng: &mut StdRng,
841 ) {
842 let chunk_size_distribution = Uniform::<usize>::new(1, 6).unwrap();
847
848 let value_distribution = Uniform::<i32>::new(-10, 10).unwrap();
851
852 for trial in 0..num_trials {
853 let mut offsets: Vec<usize> = vec![0];
854 for _ in 0..num_chunks {
855 let chunk_size = chunk_size_distribution.sample(rng);
856 offsets.push(offsets.last().unwrap() + chunk_size);
857 }
858
859 let offsets = ChunkOffsets::new(offsets.into()).unwrap();
860 let dim = offsets.dim();
861 let pivots = views::rowmajor::Owned::<f32>::from_fn(num_centers, dim, |_| {
862 value_distribution.sample(rng) as f32
863 });
864
865 let table = TransposedTable::from_parts(pivots.as_view(), offsets.clone()).unwrap();
866
867 let mut output =
868 views::rowmajor::Owned::<f32>::from_element(num_chunks, num_centers, 0.0);
869 let query: Vec<_> = (0..dim)
870 .map(|_| value_distribution.sample(rng) as f32)
871 .collect();
872
873 table.process_into::<InnerProduct>(&query, output.as_view_mut());
875
876 for chunk in 0..num_chunks {
877 let range = offsets.at(chunk);
878 let query_chunk = &query[range.clone()];
879 for center in 0..num_centers {
880 let data_chunk = &pivots.row(center)[range.clone()];
881 let expected: f32 = distance::InnerProduct::evaluate(query_chunk, data_chunk);
882 assert_eq!(
883 *output.element(chunk, center),
884 expected,
885 "failed on (chunk, center) = ({}, {}) - offsets = {:?} - trial = {}",
886 chunk,
887 center,
888 offsets,
889 trial,
890 );
891 }
892 }
893
894 table.process_into::<SquaredL2>(&query, output.as_view_mut());
896
897 for chunk in 0..num_chunks {
898 let range = offsets.at(chunk);
899 let query_chunk = &query[range.clone()];
900 for center in 0..num_centers {
901 let data_chunk = &pivots.row(center)[range.clone()];
902 let expected: f32 = distance::SquaredL2::evaluate(query_chunk, data_chunk);
903 assert_eq!(
904 *output.element(chunk, center),
905 expected,
906 "failed on (chunk, center) = ({}, {}) - offsets = {:?} - trial = {}",
907 chunk,
908 center,
909 offsets,
910 trial,
911 );
912 }
913 }
914 }
915 }
916
917 #[test]
918 fn test_process_into() {
919 let mut rng = StdRng::seed_from_u64(0x0e3cf3ba4b27e7f8);
920
921 let num_chunks_range = if cfg!(miri) { 4..5 } else { 1..5 };
922
923 let num_centers: Vec<usize> = if cfg!(miri) {
924 vec![1, 7, 16, 33, 47]
925 } else {
926 (1..48).collect()
927 };
928
929 for num_chunks in num_chunks_range {
930 for num_centers in num_centers.clone() {
931 test_process_into_impl(num_chunks, num_centers, 2, &mut rng);
932 }
933 }
934 }
935
936 #[test]
937 #[should_panic(expected = "query has the wrong number of dimensions")]
938 fn test_process_into_panics_query() {
939 let offsets = ChunkOffsets::new(Box::new([0, 1, 5])).unwrap();
940 let data = views::rowmajor::Owned::<f32>::from_element(3, 5, 0.0);
941 let table = TransposedTable::from_parts(data.as_view(), offsets).unwrap();
942 assert_eq!(table.dim(), 5);
943
944 let query = vec![0.0; table.dim() - 1];
946 let mut partials =
947 views::rowmajor::Owned::from_element(table.nchunks(), table.ncenters(), 0.0);
948 table.process_into::<InnerProduct>(&query, partials.as_view_mut());
949 }
950
951 #[test]
952 #[should_panic(expected = "output has the wrong number of rows")]
953 fn test_process_into_panics_partials_rows() {
954 let offsets = ChunkOffsets::new(Box::new([0, 1, 5])).unwrap();
955 let data = views::rowmajor::Owned::<f32>::from_element(3, 5, 0.0);
956 let table = TransposedTable::from_parts(data.as_view(), offsets).unwrap();
957 assert_eq!(table.dim(), 5);
958
959 let query = vec![0.0; table.dim()];
960 let mut partials =
962 views::rowmajor::Owned::from_element(table.nchunks() - 1, table.ncenters(), 0.0);
963 table.process_into::<InnerProduct>(&query, partials.as_view_mut());
964 }
965
966 #[test]
967 #[should_panic(expected = "output has the wrong number of columns")]
968 fn test_process_into_panics_partials_cols() {
969 let offsets = ChunkOffsets::new(Box::new([0, 1, 5])).unwrap();
970 let data = views::rowmajor::Owned::<f32>::from_element(3, 5, 0.0);
971 let table = TransposedTable::from_parts(data.as_view(), offsets).unwrap();
972 assert_eq!(table.dim(), 5);
973
974 let query = vec![0.0; table.dim()];
975 let mut partials =
977 views::rowmajor::Owned::from_element(table.nchunks(), table.ncenters() - 1, 0.0);
978 table.process_into::<InnerProduct>(&query, partials.as_view_mut());
979 }
980}