1use std::num::NonZeroUsize;
7
8use diskann_utils::views::{DenseData, MutDenseData};
9use std::ops::{Index, IndexMut};
10use thiserror::Error;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct ChunkOffsetsBase<T>
29where
30 T: DenseData<Elem = usize>,
31{
32 dim: NonZeroUsize,
37 offsets: T,
39}
40
41#[derive(Error, Debug)]
42#[non_exhaustive]
43pub enum ChunkOffsetError {
44 #[error("offsets must have a length of at least 2, found {0}")]
45 LengthNotAtLeastTwo(usize),
46 #[error("offsets must begin at 0, not {0}")]
47 DoesNotBeginWithZero(usize),
48 #[error(
49 "offsets must be strictly increasing, \
50 instead entry {start_val} at position {start} is followed by {next_val}"
51 )]
52 NonMonotonic {
53 start_val: usize,
54 start: usize,
55 next_val: usize,
56 },
57}
58
59#[derive(Error, Debug)]
61#[error("num_chunks {num_chunks} must not exceed dim {dim}")]
62pub struct PartitionError {
63 pub num_chunks: usize,
64 pub dim: usize,
65}
66
67#[derive(Error, Debug)]
69#[non_exhaustive]
70pub enum PartitionIntoError {
71 #[error("scratch must have a length of at least 2, found {0}")]
72 ScratchTooSmall(usize),
73 #[error(transparent)]
74 PartitionError(#[from] PartitionError),
75}
76
77impl<T> ChunkOffsetsBase<T>
78where
79 T: DenseData<Elem = usize>,
80{
81 pub fn new(offsets: T) -> Result<Self, ChunkOffsetError> {
87 let slice = offsets.as_slice();
88
89 let len = slice.len();
91 if len < 2 {
92 return Err(ChunkOffsetError::LengthNotAtLeastTwo(len));
93 }
94
95 let start = slice[0];
97 if start != 0 {
98 return Err(ChunkOffsetError::DoesNotBeginWithZero(start));
99 }
100
101 let mut last: NonZeroUsize = match NonZeroUsize::new(slice[1]) {
108 Some(x) => Ok(x),
109 None => Err(ChunkOffsetError::NonMonotonic {
110 start_val: start,
111 start: 0,
112 next_val: 0,
113 }),
114 }?;
115
116 for i in 2..slice.len() {
121 let start_val = slice[i - 1];
122 let next_val = NonZeroUsize::new(slice[i]);
123 last = match next_val {
124 Some(next_val) => {
125 if start_val >= next_val.get() {
126 Err(ChunkOffsetError::NonMonotonic {
127 start_val,
128 start: i - 1,
129 next_val: next_val.get(),
130 })
131 } else {
132 Ok(next_val)
133 }
134 }
135 None => Err(ChunkOffsetError::NonMonotonic {
137 start_val,
138 start: i - 1,
139 next_val: 0,
140 }),
141 }?;
142 }
143
144 Ok(Self { dim: last, offsets })
146 }
147
148 pub fn len(&self) -> usize {
152 debug_assert!(self.offsets.as_slice().len() >= 2);
155 self.offsets.as_slice().len() - 1
156 }
157
158 pub fn is_empty(&self) -> bool {
160 false
162 }
163
164 pub fn dim(&self) -> usize {
166 self.dim.get()
167 }
168
169 pub fn dim_nonzero(&self) -> NonZeroUsize {
176 self.dim
177 }
178
179 #[expect(
185 clippy::panic,
186 reason = "this is documented to panic for out-of-bounds `i`"
187 )]
188 pub fn at(&self, i: usize) -> core::ops::Range<usize> {
189 match self.get(i) {
190 Some(range) => range,
191 None => panic!("index {i} must be less than len {}", self.len()),
192 }
193 }
194
195 pub fn get(&self, i: usize) -> Option<core::ops::Range<usize>> {
199 if i < self.len() {
200 let slice = self.offsets.as_slice();
201 Some(slice[i]..slice[i + 1])
202 } else {
203 None
204 }
205 }
206
207 pub fn as_view(&self) -> ChunkOffsetsView<'_> {
209 ChunkOffsetsBase {
210 dim: self.dim,
211 offsets: self.offsets.as_slice(),
212 }
213 }
214
215 pub fn to_owned(&self) -> ChunkOffsets {
217 ChunkOffsetsBase {
218 dim: self.dim,
219 offsets: self.offsets.as_slice().into(),
220 }
221 }
222
223 pub fn as_slice(&self) -> &[usize] {
225 self.offsets.as_slice()
226 }
227
228 pub fn max_chunk_dim(&self) -> NonZeroUsize {
230 let mut max = NonZeroUsize::MIN;
231 let mut itr = self.offsets.as_slice().iter();
232
233 let Some(mut previous) = itr.next() else {
234 return max;
237 };
238
239 for next in itr {
240 if let Some(dim) = NonZeroUsize::new(next - previous) {
242 max = max.max(dim);
243 }
244
245 previous = next;
246 }
247
248 max
249 }
250}
251
252pub type ChunkOffsetsView<'a> = ChunkOffsetsBase<&'a [usize]>;
253pub type ChunkOffsets = ChunkOffsetsBase<Box<[usize]>>;
254
255impl<'a> From<ChunkOffsetsView<'a>> for &'a [usize] {
257 fn from(view: ChunkOffsetsView<'a>) -> Self {
258 view.offsets
259 }
260}
261
262impl ChunkOffsets {
263 pub fn partition(dim: NonZeroUsize, num_chunks: NonZeroUsize) -> Result<Self, PartitionError> {
270 if num_chunks.get() > dim.get() {
271 return Err(PartitionError {
272 num_chunks: num_chunks.get(),
273 dim: dim.get(),
274 });
275 }
276 let mut offsets = vec![0usize; num_chunks.get() + 1].into_boxed_slice();
277 fill_chunk_offsets(dim, &mut offsets);
278 Ok(Self { dim, offsets })
279 }
280}
281
282impl<'a> ChunkOffsetsView<'a> {
283 pub fn partition_into(
291 dim: NonZeroUsize,
292 scratch: &'a mut [usize],
293 ) -> Result<Self, PartitionIntoError> {
294 if scratch.len() < 2 {
295 return Err(PartitionIntoError::ScratchTooSmall(scratch.len()));
296 }
297 let num_chunks = scratch.len() - 1;
298 if num_chunks > dim.get() {
299 return Err(PartitionError {
300 num_chunks,
301 dim: dim.get(),
302 }
303 .into());
304 }
305
306 fill_chunk_offsets(dim, scratch);
307 Ok(Self {
308 dim,
309 offsets: scratch,
310 })
311 }
312}
313
314fn fill_chunk_offsets(dimensions: NonZeroUsize, offsets: &mut [usize]) {
326 let num_chunks = offsets.len() - 1;
327 let dimensions = dimensions.get();
328 let mut chunk_offset: usize = 0;
329 offsets[0] = chunk_offset;
330 for chunk_index in 0..num_chunks {
331 chunk_offset += dimensions / num_chunks;
332 if chunk_index < (dimensions % num_chunks) {
333 chunk_offset += 1;
334 }
335 offsets[chunk_index + 1] = chunk_offset;
336 }
337}
338
339#[derive(Debug, Clone, Copy)]
349pub struct ChunkViewImpl<'a, T>
350where
351 T: DenseData,
352{
353 data: T,
354 offsets: ChunkOffsetsView<'a>,
355}
356
357#[derive(Error, Debug)]
358#[non_exhaustive]
359#[error(
360 "error in chunk view construction, got a slice of length {got} but \
361 the provided chunking schema expects a length of {should}"
362)]
363pub struct ChunkViewError {
364 got: usize,
365 should: usize,
366}
367
368impl<'a, T> ChunkViewImpl<'a, T>
369where
370 T: DenseData,
371{
372 pub fn new<U>(data: U, offsets: ChunkOffsetsView<'a>) -> Result<Self, ChunkViewError>
376 where
377 T: From<U>,
378 {
379 let data: T = data.into();
380
381 let got = data.as_slice().len();
385 let should = offsets.dim();
386 if got != should {
387 Err(ChunkViewError { got, should })
388 } else {
389 Ok(Self { data, offsets })
390 }
391 }
392
393 pub fn len(&self) -> usize {
395 self.offsets.len()
396 }
397
398 pub fn is_empty(&self) -> bool {
399 self.offsets.is_empty()
400 }
401}
402
403impl<T> Index<usize> for ChunkViewImpl<'_, T>
409where
410 T: DenseData,
411{
412 type Output = [T::Elem];
413
414 fn index(&self, i: usize) -> &Self::Output {
415 &(self.data.as_slice())[self.offsets.at(i)]
416 }
417}
418
419impl<T> IndexMut<usize> for ChunkViewImpl<'_, T>
425where
426 T: MutDenseData,
427{
428 fn index_mut(&mut self, i: usize) -> &mut Self::Output {
429 &mut (self.data.as_mut_slice())[self.offsets.at(i)]
430 }
431}
432
433pub type ChunkView<'a, T> = ChunkViewImpl<'a, &'a [T]>;
434pub type MutChunkView<'a, T> = ChunkViewImpl<'a, &'a mut [T]>;
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use diskann_utils::lazy_format;
440
441 fn is_copyable<T: Copy>(_x: T) -> bool {
445 true
446 }
447
448 #[test]
453 fn chunk_offset_happy_path() {
454 let offsets_raw: Vec<usize> = vec![0, 1, 3, 6, 10, 12, 13, 14];
455 let offsets = ChunkOffsetsView::new(offsets_raw.as_slice()).unwrap();
456
457 assert_eq!(offsets.len(), offsets_raw.len() - 1);
458 assert_eq!(offsets.dim(), *offsets_raw.last().unwrap());
459 assert!(!offsets.is_empty());
460
461 assert_eq!(offsets.at(0), 0..1);
462 assert_eq!(offsets.get(0).unwrap(), 0..1);
463 assert_eq!(offsets.at(1), 1..3);
464 assert_eq!(offsets.get(1).unwrap(), 1..3);
465 assert_eq!(offsets.at(2), 3..6);
466 assert_eq!(offsets.get(2).unwrap(), 3..6);
467 assert_eq!(offsets.at(3), 6..10);
468 assert_eq!(offsets.get(3).unwrap(), 6..10);
469 assert_eq!(offsets.at(4), 10..12);
470 assert_eq!(offsets.get(4).unwrap(), 10..12);
471 assert_eq!(offsets.at(5), 12..13);
472 assert_eq!(offsets.get(5).unwrap(), 12..13);
473 assert_eq!(offsets.at(6), 13..14);
474 assert_eq!(offsets.get(6).unwrap(), 13..14);
475
476 assert!(offsets.get(7).is_none());
477 assert!(offsets.get(8).is_none());
478
479 assert!(is_copyable(offsets));
481 assert_eq!(offsets.as_slice(), offsets_raw.as_slice());
483
484 let offsets_owned = offsets.to_owned();
486 assert_eq!(offsets_owned.as_slice(), offsets_raw.as_slice());
487 assert_ne!(
488 offsets_owned.as_slice().as_ptr(),
489 offsets_raw.as_slice().as_ptr()
490 );
491 assert_eq!(offsets_owned.dim, offsets.dim);
492
493 let offsets_view = offsets_owned.as_view();
495 assert_eq!(offsets_view, offsets);
496 assert_eq!(
498 offsets_view.as_slice().as_ptr(),
499 offsets_owned.as_slice().as_ptr()
500 );
501
502 assert_eq!(offsets.max_chunk_dim().get(), 4);
504 }
505
506 #[test]
507 fn chunk_offset_max_dim() {
508 let offsets = ChunkOffsetsView::new(&[0, 10, 11, 14]).unwrap();
509 assert_eq!(offsets.max_chunk_dim().get(), 10);
510
511 let offsets = ChunkOffsetsView::new(&[0, 1, 2, 3]).unwrap();
512 assert_eq!(offsets.max_chunk_dim().get(), 1);
513
514 let offsets = ChunkOffsetsView::new(&[0, 1, 2, 5, 20, 21]).unwrap();
515 assert_eq!(offsets.max_chunk_dim().get(), 15);
516 }
517
518 #[test]
519 #[should_panic(expected = "index 5 must be less than len 3")]
520 fn chunk_offset_indexing_panic() {
521 let offsets = ChunkOffsets::new(Box::new([0, 1, 2, 3])).unwrap();
522 assert!(offsets.get(5).is_none());
523
524 let _ = offsets.at(5);
526 }
527
528 #[test]
530 fn chunk_offset_construction_errors() {
531 let offsets = ChunkOffsets::new(Box::new([]));
533 assert_eq!(
534 offsets.unwrap_err().to_string(),
535 "offsets must have a length of at least 2, found 0"
536 );
537
538 let offsets = ChunkOffsets::new(Box::new([0]));
540 assert_eq!(
541 offsets.unwrap_err().to_string(),
542 "offsets must have a length of at least 2, found 1"
543 );
544
545 let offsets = ChunkOffsets::new(Box::new([10, 11, 12, 13]));
547 assert_eq!(
548 offsets.unwrap_err().to_string(),
549 "offsets must begin at 0, not 10"
550 );
551
552 let offsets = ChunkOffsets::new(Box::new([0, 10, 20, 30, 30, 40, 41]));
554 assert_eq!(
555 offsets.unwrap_err().to_string(),
556 "offsets must be strictly increasing, instead entry 30 at position 3 \
557 is followed by 30"
558 );
559
560 let offsets = ChunkOffsets::new(Box::new([0, 10, 9, 10, 20]));
562 assert_eq!(
563 offsets.unwrap_err().to_string(),
564 "offsets must be strictly increasing, instead entry 10 at position 1 \
565 is followed by 9"
566 );
567
568 let offsets = ChunkOffsets::new(Box::new([0, 10, 11, 12, 0]));
570 assert_eq!(
571 offsets.unwrap_err().to_string(),
572 "offsets must be strictly increasing, instead entry 12 at position 3 \
573 is followed by 0"
574 );
575
576 let offsets = ChunkOffsets::new(Box::new([0, 0, 11, 12, 20]));
578 assert_eq!(
579 offsets.unwrap_err().to_string(),
580 "offsets must be strictly increasing, instead entry 0 at position 0 \
581 is followed by 0"
582 );
583 }
584
585 #[test]
590 fn partition_happy_path() {
591 let nz = |x: usize| NonZeroUsize::new(x).unwrap();
592
593 let offsets = ChunkOffsets::partition(nz(9), nz(3)).unwrap();
595 assert_eq!(offsets.as_slice(), &[0, 3, 6, 9]);
596 assert_eq!(offsets.dim(), 9);
597 assert_eq!(offsets.len(), 3);
598
599 let offsets = ChunkOffsets::partition(nz(8), nz(3)).unwrap();
601 assert_eq!(offsets.as_slice(), &[0, 3, 6, 8]);
602
603 let offsets = ChunkOffsets::partition(nz(5), nz(1)).unwrap();
605 assert_eq!(offsets.as_slice(), &[0, 5]);
606
607 let offsets = ChunkOffsets::partition(nz(4), nz(4)).unwrap();
609 assert_eq!(offsets.as_slice(), &[0, 1, 2, 3, 4]);
610
611 let mut scratch = [0usize; 4];
614 let view = ChunkOffsetsView::partition_into(nz(8), &mut scratch).unwrap();
615 assert_eq!(view.as_slice(), &[0, 3, 6, 8]);
616 assert_eq!(view.dim(), 8);
617 assert_eq!(view.len(), 3);
618 assert_eq!(scratch.as_slice(), &[0, 3, 6, 8]);
619 }
620
621 #[test]
622 fn partition_construction_errors() {
623 let nz = |x: usize| NonZeroUsize::new(x).unwrap();
624
625 let err = ChunkOffsets::partition(nz(3), nz(5)).unwrap_err();
627 assert!(
628 matches!(
629 err,
630 PartitionError {
631 num_chunks: 5,
632 dim: 3
633 }
634 ),
635 "expected TooManyChunks, got {err:?}"
636 );
637
638 let mut too_short = [0usize; 1];
640 let err = ChunkOffsetsView::partition_into(nz(8), &mut too_short).unwrap_err();
641 assert!(matches!(err, PartitionIntoError::ScratchTooSmall(1)));
642
643 let mut scratch = [0usize; 6];
645 let err = ChunkOffsetsView::partition_into(nz(3), &mut scratch).unwrap_err();
646 assert!(matches!(
647 err,
648 PartitionIntoError::PartitionError(PartitionError {
649 num_chunks: 5,
650 dim: 3
651 })
652 ));
653 }
654
655 fn check_chunk_view<T>(
660 view: &ChunkViewImpl<'_, T>,
661 data: &[i32],
662 offsets: &[usize],
663 context: &dyn std::fmt::Display,
664 ) where
665 T: DenseData<Elem = i32>,
666 {
667 assert_eq!(view.len(), offsets.len() - 1, "{}", context);
668
669 for i in 0..view.len() {
671 let context = lazy_format!("start = {}, {}", i, context);
672 let start = offsets[i];
673 let stop = offsets[i + 1];
674
675 let expected = &data[start..stop];
676 let retrieved = &view[i];
677
678 assert_eq!(retrieved, expected, "{}", context);
679 }
680 }
681
682 #[test]
683 fn test_immutable_chunkview() {
684 let data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
685 let offsets: Vec<usize> = vec![0, 3, 5, 9, 10];
688
689 let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
690 let chunk_view = ChunkView::new(data.as_slice(), chunks).unwrap();
691
692 assert_eq!(chunk_view.len(), offsets.len() - 1);
693 assert_eq!(chunk_view.len(), chunks.len());
694
695 assert!(is_copyable(chunk_view));
696 let context = lazy_format!("chunkview happy path");
697 check_chunk_view(&chunk_view, &data, &offsets, &context);
698 }
699
700 #[test]
701 fn test_chunkview_construction_error() {
702 let data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
703 let offsets: Vec<usize> = vec![0, 3, 5, 9]; let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
708 let chunk_view = ChunkView::new(data.as_slice(), chunks);
709 assert!(chunk_view.is_err());
710 assert_eq!(
711 chunk_view.unwrap_err().to_string(),
712 "error in chunk view construction, got a slice of length 10 but \
713 the provided chunking schema expects a length of 9"
714 );
715 }
716
717 #[test]
718 fn test_mutable_chunkview() {
719 let mut data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
720 let offsets: Vec<usize> = vec![0, 3, 5, 9, 10];
723
724 let data_clone = data.clone();
727
728 let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
729 let mut chunk_view = MutChunkView::new(data.as_mut_slice(), chunks).unwrap();
730
731 assert_eq!(chunk_view.len(), offsets.len() - 1);
732 assert_eq!(chunk_view.len(), chunks.len());
733
734 let context = lazy_format!("mutchunkview happy path");
735 check_chunk_view(&chunk_view, &data_clone, &offsets, &context);
736
737 for i in 0..chunk_view.len() {
739 let i_i32: i32 = i.try_into().unwrap();
740
741 chunk_view[i].iter_mut().for_each(|d| *d = i_i32);
742 }
743
744 assert_eq!(data[0], 0);
746 assert_eq!(data[1], 0);
747 assert_eq!(data[2], 0);
748
749 assert_eq!(data[3], 1);
751 assert_eq!(data[4], 1);
752
753 assert_eq!(data[5], 2);
755 assert_eq!(data[6], 2);
756 assert_eq!(data[7], 2);
757 assert_eq!(data[8], 2);
758
759 assert_eq!(data[9], 3);
761 }
762}