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 pub fn at(&self, i: usize) -> core::ops::Range<usize> {
185 assert!(
186 i < self.len(),
187 "index {i} must be less than len {}",
188 self.len()
189 );
190 let slice = self.offsets.as_slice();
191 slice[i]..slice[i + 1]
192 }
193
194 pub fn as_view(&self) -> ChunkOffsetsView<'_> {
196 ChunkOffsetsBase {
197 dim: self.dim,
198 offsets: self.offsets.as_slice(),
199 }
200 }
201
202 pub fn to_owned(&self) -> ChunkOffsets {
204 ChunkOffsetsBase {
205 dim: self.dim,
206 offsets: self.offsets.as_slice().into(),
207 }
208 }
209
210 pub fn as_slice(&self) -> &[usize] {
212 self.offsets.as_slice()
213 }
214}
215
216pub type ChunkOffsetsView<'a> = ChunkOffsetsBase<&'a [usize]>;
217pub type ChunkOffsets = ChunkOffsetsBase<Box<[usize]>>;
218
219impl<'a> From<ChunkOffsetsView<'a>> for &'a [usize] {
221 fn from(view: ChunkOffsetsView<'a>) -> Self {
222 view.offsets
223 }
224}
225
226impl ChunkOffsets {
227 pub fn partition(dim: NonZeroUsize, num_chunks: NonZeroUsize) -> Result<Self, PartitionError> {
234 if num_chunks.get() > dim.get() {
235 return Err(PartitionError {
236 num_chunks: num_chunks.get(),
237 dim: dim.get(),
238 });
239 }
240 let mut offsets = vec![0usize; num_chunks.get() + 1].into_boxed_slice();
241 fill_chunk_offsets(dim, &mut offsets);
242 Ok(Self { dim, offsets })
243 }
244}
245
246impl<'a> ChunkOffsetsView<'a> {
247 pub fn partition_into(
255 dim: NonZeroUsize,
256 scratch: &'a mut [usize],
257 ) -> Result<Self, PartitionIntoError> {
258 if scratch.len() < 2 {
259 return Err(PartitionIntoError::ScratchTooSmall(scratch.len()));
260 }
261 let num_chunks = scratch.len() - 1;
262 if num_chunks > dim.get() {
263 return Err(PartitionError {
264 num_chunks,
265 dim: dim.get(),
266 }
267 .into());
268 }
269
270 fill_chunk_offsets(dim, scratch);
271 Ok(Self {
272 dim,
273 offsets: scratch,
274 })
275 }
276}
277
278fn fill_chunk_offsets(dimensions: NonZeroUsize, offsets: &mut [usize]) {
290 let num_chunks = offsets.len() - 1;
291 let dimensions = dimensions.get();
292 let mut chunk_offset: usize = 0;
293 offsets[0] = chunk_offset;
294 for chunk_index in 0..num_chunks {
295 chunk_offset += dimensions / num_chunks;
296 if chunk_index < (dimensions % num_chunks) {
297 chunk_offset += 1;
298 }
299 offsets[chunk_index + 1] = chunk_offset;
300 }
301}
302
303#[derive(Debug, Clone, Copy)]
313pub struct ChunkViewImpl<'a, T>
314where
315 T: DenseData,
316{
317 data: T,
318 offsets: ChunkOffsetsView<'a>,
319}
320
321#[derive(Error, Debug)]
322#[non_exhaustive]
323#[error(
324 "error in chunk view construction, got a slice of length {got} but \
325 the provided chunking schema expects a length of {should}"
326)]
327pub struct ChunkViewError {
328 got: usize,
329 should: usize,
330}
331
332impl<'a, T> ChunkViewImpl<'a, T>
333where
334 T: DenseData,
335{
336 pub fn new<U>(data: U, offsets: ChunkOffsetsView<'a>) -> Result<Self, ChunkViewError>
340 where
341 T: From<U>,
342 {
343 let data: T = data.into();
344
345 let got = data.as_slice().len();
349 let should = offsets.dim();
350 if got != should {
351 Err(ChunkViewError { got, should })
352 } else {
353 Ok(Self { data, offsets })
354 }
355 }
356
357 pub fn len(&self) -> usize {
359 self.offsets.len()
360 }
361
362 pub fn is_empty(&self) -> bool {
363 self.offsets.is_empty()
364 }
365}
366
367impl<T> Index<usize> for ChunkViewImpl<'_, T>
373where
374 T: DenseData,
375{
376 type Output = [T::Elem];
377
378 fn index(&self, i: usize) -> &Self::Output {
379 &(self.data.as_slice())[self.offsets.at(i)]
380 }
381}
382
383impl<T> IndexMut<usize> for ChunkViewImpl<'_, T>
389where
390 T: MutDenseData,
391{
392 fn index_mut(&mut self, i: usize) -> &mut Self::Output {
393 &mut (self.data.as_mut_slice())[self.offsets.at(i)]
394 }
395}
396
397pub type ChunkView<'a, T> = ChunkViewImpl<'a, &'a [T]>;
398pub type MutChunkView<'a, T> = ChunkViewImpl<'a, &'a mut [T]>;
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403 use diskann_utils::lazy_format;
404
405 fn is_copyable<T: Copy>(_x: T) -> bool {
409 true
410 }
411
412 #[test]
417 fn chunk_offset_happy_path() {
418 let offsets_raw: Vec<usize> = vec![0, 1, 3, 6, 10, 12, 13, 14];
419 let offsets = ChunkOffsetsView::new(offsets_raw.as_slice()).unwrap();
420
421 assert_eq!(offsets.len(), offsets_raw.len() - 1);
422 assert_eq!(offsets.dim(), *offsets_raw.last().unwrap());
423 assert!(!offsets.is_empty());
424
425 assert_eq!(offsets.at(0), 0..1);
426 assert_eq!(offsets.at(1), 1..3);
427 assert_eq!(offsets.at(2), 3..6);
428 assert_eq!(offsets.at(3), 6..10);
429 assert_eq!(offsets.at(4), 10..12);
430 assert_eq!(offsets.at(5), 12..13);
431 assert_eq!(offsets.at(6), 13..14);
432
433 assert!(is_copyable(offsets));
435 assert_eq!(offsets.as_slice(), offsets_raw.as_slice());
437
438 let offsets_owned = offsets.to_owned();
440 assert_eq!(offsets_owned.as_slice(), offsets_raw.as_slice());
441 assert_ne!(
442 offsets_owned.as_slice().as_ptr(),
443 offsets_raw.as_slice().as_ptr()
444 );
445 assert_eq!(offsets_owned.dim, offsets.dim);
446
447 let offsets_view = offsets_owned.as_view();
449 assert_eq!(offsets_view, offsets);
450 assert_eq!(
452 offsets_view.as_slice().as_ptr(),
453 offsets_owned.as_slice().as_ptr()
454 );
455 }
456
457 #[test]
458 #[should_panic(expected = "index 5 must be less than len 3")]
459 fn chunk_offset_indexing_panic() {
460 let offsets = ChunkOffsets::new(Box::new([0, 1, 2, 3])).unwrap();
461
462 let _ = offsets.at(5);
464 }
465
466 #[test]
468 fn chunk_offset_construction_errors() {
469 let offsets = ChunkOffsets::new(Box::new([]));
471 assert_eq!(
472 offsets.unwrap_err().to_string(),
473 "offsets must have a length of at least 2, found 0"
474 );
475
476 let offsets = ChunkOffsets::new(Box::new([0]));
478 assert_eq!(
479 offsets.unwrap_err().to_string(),
480 "offsets must have a length of at least 2, found 1"
481 );
482
483 let offsets = ChunkOffsets::new(Box::new([10, 11, 12, 13]));
485 assert_eq!(
486 offsets.unwrap_err().to_string(),
487 "offsets must begin at 0, not 10"
488 );
489
490 let offsets = ChunkOffsets::new(Box::new([0, 10, 20, 30, 30, 40, 41]));
492 assert_eq!(
493 offsets.unwrap_err().to_string(),
494 "offsets must be strictly increasing, instead entry 30 at position 3 \
495 is followed by 30"
496 );
497
498 let offsets = ChunkOffsets::new(Box::new([0, 10, 9, 10, 20]));
500 assert_eq!(
501 offsets.unwrap_err().to_string(),
502 "offsets must be strictly increasing, instead entry 10 at position 1 \
503 is followed by 9"
504 );
505
506 let offsets = ChunkOffsets::new(Box::new([0, 10, 11, 12, 0]));
508 assert_eq!(
509 offsets.unwrap_err().to_string(),
510 "offsets must be strictly increasing, instead entry 12 at position 3 \
511 is followed by 0"
512 );
513
514 let offsets = ChunkOffsets::new(Box::new([0, 0, 11, 12, 20]));
516 assert_eq!(
517 offsets.unwrap_err().to_string(),
518 "offsets must be strictly increasing, instead entry 0 at position 0 \
519 is followed by 0"
520 );
521 }
522
523 #[test]
528 fn partition_happy_path() {
529 let nz = |x: usize| NonZeroUsize::new(x).unwrap();
530
531 let offsets = ChunkOffsets::partition(nz(9), nz(3)).unwrap();
533 assert_eq!(offsets.as_slice(), &[0, 3, 6, 9]);
534 assert_eq!(offsets.dim(), 9);
535 assert_eq!(offsets.len(), 3);
536
537 let offsets = ChunkOffsets::partition(nz(8), nz(3)).unwrap();
539 assert_eq!(offsets.as_slice(), &[0, 3, 6, 8]);
540
541 let offsets = ChunkOffsets::partition(nz(5), nz(1)).unwrap();
543 assert_eq!(offsets.as_slice(), &[0, 5]);
544
545 let offsets = ChunkOffsets::partition(nz(4), nz(4)).unwrap();
547 assert_eq!(offsets.as_slice(), &[0, 1, 2, 3, 4]);
548
549 let mut scratch = [0usize; 4];
552 let view = ChunkOffsetsView::partition_into(nz(8), &mut scratch).unwrap();
553 assert_eq!(view.as_slice(), &[0, 3, 6, 8]);
554 assert_eq!(view.dim(), 8);
555 assert_eq!(view.len(), 3);
556 assert_eq!(scratch.as_slice(), &[0, 3, 6, 8]);
557 }
558
559 #[test]
560 fn partition_construction_errors() {
561 let nz = |x: usize| NonZeroUsize::new(x).unwrap();
562
563 let err = ChunkOffsets::partition(nz(3), nz(5)).unwrap_err();
565 assert!(
566 matches!(
567 err,
568 PartitionError {
569 num_chunks: 5,
570 dim: 3
571 }
572 ),
573 "expected TooManyChunks, got {err:?}"
574 );
575
576 let mut too_short = [0usize; 1];
578 let err = ChunkOffsetsView::partition_into(nz(8), &mut too_short).unwrap_err();
579 assert!(matches!(err, PartitionIntoError::ScratchTooSmall(1)));
580
581 let mut scratch = [0usize; 6];
583 let err = ChunkOffsetsView::partition_into(nz(3), &mut scratch).unwrap_err();
584 assert!(matches!(
585 err,
586 PartitionIntoError::PartitionError(PartitionError {
587 num_chunks: 5,
588 dim: 3
589 })
590 ));
591 }
592
593 fn check_chunk_view<T>(
598 view: &ChunkViewImpl<'_, T>,
599 data: &[i32],
600 offsets: &[usize],
601 context: &dyn std::fmt::Display,
602 ) where
603 T: DenseData<Elem = i32>,
604 {
605 assert_eq!(view.len(), offsets.len() - 1, "{}", context);
606
607 for i in 0..view.len() {
609 let context = lazy_format!("start = {}, {}", i, context);
610 let start = offsets[i];
611 let stop = offsets[i + 1];
612
613 let expected = &data[start..stop];
614 let retrieved = &view[i];
615
616 assert_eq!(retrieved, expected, "{}", context);
617 }
618 }
619
620 #[test]
621 fn test_immutable_chunkview() {
622 let data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
623 let offsets: Vec<usize> = vec![0, 3, 5, 9, 10];
626
627 let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
628 let chunk_view = ChunkView::new(data.as_slice(), chunks).unwrap();
629
630 assert_eq!(chunk_view.len(), offsets.len() - 1);
631 assert_eq!(chunk_view.len(), chunks.len());
632
633 assert!(is_copyable(chunk_view));
634 let context = lazy_format!("chunkview happy path");
635 check_chunk_view(&chunk_view, &data, &offsets, &context);
636 }
637
638 #[test]
639 fn test_chunkview_construction_error() {
640 let data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
641 let offsets: Vec<usize> = vec![0, 3, 5, 9]; let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
646 let chunk_view = ChunkView::new(data.as_slice(), chunks);
647 assert!(chunk_view.is_err());
648 assert_eq!(
649 chunk_view.unwrap_err().to_string(),
650 "error in chunk view construction, got a slice of length 10 but \
651 the provided chunking schema expects a length of 9"
652 );
653 }
654
655 #[test]
656 fn test_mutable_chunkview() {
657 let mut data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
658 let offsets: Vec<usize> = vec![0, 3, 5, 9, 10];
661
662 let data_clone = data.clone();
665
666 let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
667 let mut chunk_view = MutChunkView::new(data.as_mut_slice(), chunks).unwrap();
668
669 assert_eq!(chunk_view.len(), offsets.len() - 1);
670 assert_eq!(chunk_view.len(), chunks.len());
671
672 let context = lazy_format!("mutchunkview happy path");
673 check_chunk_view(&chunk_view, &data_clone, &offsets, &context);
674
675 for i in 0..chunk_view.len() {
677 let i_i32: i32 = i.try_into().unwrap();
678
679 chunk_view[i].iter_mut().for_each(|d| *d = i_i32);
680 }
681
682 assert_eq!(data[0], 0);
684 assert_eq!(data[1], 0);
685 assert_eq!(data[2], 0);
686
687 assert_eq!(data[3], 1);
689 assert_eq!(data[4], 1);
690
691 assert_eq!(data[5], 2);
693 assert_eq!(data[6], 2);
694 assert_eq!(data[7], 2);
695 assert_eq!(data[8], 2);
696
697 assert_eq!(data[9], 3);
699 }
700}