1use crate::deepsize::{Context, DeepSizeOf};
63use crate::{Error, Result};
64use arrow_buffer::NullBuffer;
65
66pub const DEFAULT_BLOCK_ROWS: u32 = 64 * 1024;
69
70pub const MAX_DESTINATIONS: u32 = u16::MAX as u32 + 1;
73
74const COUNTS_MAGIC: &[u8; 4] = b"LSPC";
75const COUNTS_VERSION: u32 = 1;
76const COUNTS_HEADER_BYTES: usize = 4 + 4 + 4 + 4 + 4 + 8;
78const REPR_DENSE: u32 = 0;
83
84fn corrupt(message: impl Into<String>) -> Error {
85 Error::corrupt_file_named("stable_partition_counts", message)
86}
87
88#[derive(Clone, Debug, PartialEq, Eq)]
96pub struct CountsMatrix {
97 num_destinations: u32,
99 block_rows: u32,
101 total_rows: u64,
103 cumulative: Vec<u32>,
105}
106
107impl CountsMatrix {
108 pub fn num_destinations(&self) -> u32 {
109 self.num_destinations
110 }
111
112 pub fn block_rows(&self) -> u32 {
113 self.block_rows
114 }
115
116 pub fn total_rows(&self) -> u64 {
117 self.total_rows
118 }
119
120 pub fn num_blocks(&self) -> usize {
121 self.cumulative.len() / self.num_destinations as usize
122 }
123
124 pub fn block_of(&self, row: u64) -> usize {
126 (row / u64::from(self.block_rows)) as usize
127 }
128
129 pub fn block_range(&self, block: usize) -> std::ops::Range<u64> {
131 let start = block as u64 * u64::from(self.block_rows);
132 let end = (start + u64::from(self.block_rows)).min(self.total_rows);
133 start..end
134 }
135
136 fn block_row(&self, block: usize) -> &[u32] {
137 let m = self.num_destinations as usize;
138 &self.cumulative[block * m..(block + 1) * m]
139 }
140
141 pub fn count_before(&self, block: usize, label: u16) -> u32 {
144 if block == 0 {
145 0
146 } else {
147 self.block_row(block - 1)[label as usize]
148 }
149 }
150
151 pub fn counters_at_block(&self, block: usize) -> Vec<u32> {
154 if block == 0 {
155 vec![0; self.num_destinations as usize]
156 } else {
157 self.block_row(block - 1).to_vec()
158 }
159 }
160
161 pub fn total(&self, label: u16) -> u32 {
163 let blocks = self.num_blocks();
164 if blocks == 0 {
165 0
166 } else {
167 self.block_row(blocks - 1)[label as usize]
168 }
169 }
170
171 pub fn total_live_rows(&self) -> u64 {
173 let blocks = self.num_blocks();
174 if blocks == 0 {
175 0
176 } else {
177 self.block_row(blocks - 1)
178 .iter()
179 .map(|&c| u64::from(c))
180 .sum()
181 }
182 }
183
184 pub fn deleted_through(&self, block: usize) -> u64 {
186 let rows_through = self.block_range(block).end;
187 let live_through: u64 = self.block_row(block).iter().map(|&c| u64::from(c)).sum();
188 rows_through - live_through
189 }
190
191 pub fn validate(&self) -> Result<()> {
197 let mut prev_live = 0u64;
198 for block in 0..self.num_blocks() {
199 if block > 0 {
200 let (prev, cur) = (self.block_row(block - 1), self.block_row(block));
201 if prev.iter().zip(cur).any(|(p, c)| c < p) {
202 return Err(corrupt(format!(
203 "cumulative counts decrease at block {block}"
204 )));
205 }
206 }
207 let live_through: u64 = self.block_row(block).iter().map(|&c| u64::from(c)).sum();
208 let range = self.block_range(block);
209 let block_len = range.end - range.start;
210 if live_through - prev_live > block_len {
211 return Err(corrupt(format!(
212 "block {block} accounts for more live rows than its {block_len} rows"
213 )));
214 }
215 prev_live = live_through;
216 }
217 Ok(())
218 }
219
220 pub fn encode(&self) -> Vec<u8> {
224 let mut buf = Vec::with_capacity(COUNTS_HEADER_BYTES + self.cumulative.len() * 4);
225 buf.extend_from_slice(COUNTS_MAGIC);
226 buf.extend_from_slice(&COUNTS_VERSION.to_le_bytes());
227 buf.extend_from_slice(&REPR_DENSE.to_le_bytes());
228 buf.extend_from_slice(&self.num_destinations.to_le_bytes());
229 buf.extend_from_slice(&self.block_rows.to_le_bytes());
230 buf.extend_from_slice(&self.total_rows.to_le_bytes());
231 for count in &self.cumulative {
232 buf.extend_from_slice(&count.to_le_bytes());
233 }
234 buf
235 }
236
237 pub fn decode(buf: &[u8]) -> Result<Self> {
242 let header: &[u8; COUNTS_HEADER_BYTES] = buf
243 .get(..COUNTS_HEADER_BYTES)
244 .and_then(|header| header.try_into().ok())
245 .ok_or_else(|| {
246 corrupt(format!(
247 "counts buffer is {} bytes, shorter than the {COUNTS_HEADER_BYTES}-byte header",
248 buf.len()
249 ))
250 })?;
251 if &header[0..4] != COUNTS_MAGIC {
252 return Err(corrupt("counts buffer has a bad magic number"));
253 }
254 let version = u32::from_le_bytes(header[4..8].try_into().unwrap());
255 if version != COUNTS_VERSION {
256 return Err(corrupt(format!("unsupported counts version {version}")));
257 }
258 let repr = u32::from_le_bytes(header[8..12].try_into().unwrap());
259 if repr != REPR_DENSE {
260 return Err(corrupt(format!(
261 "unsupported counts representation {repr}; this reader only supports the dense grid ({REPR_DENSE})"
262 )));
263 }
264 let num_destinations = u32::from_le_bytes(header[12..16].try_into().unwrap());
265 let block_rows = u32::from_le_bytes(header[16..20].try_into().unwrap());
266 let total_rows = u64::from_le_bytes(header[20..28].try_into().unwrap());
267 Self::check_shape(num_destinations, block_rows)?;
268 let num_blocks = total_rows.div_ceil(u64::from(block_rows));
269 num_blocks
270 .checked_mul(u64::from(num_destinations))
271 .and_then(|cells| cells.checked_mul(4))
272 .filter(|&bytes| bytes == (buf.len() - COUNTS_HEADER_BYTES) as u64)
273 .ok_or_else(|| {
274 corrupt(format!(
275 "counts buffer of {} bytes does not match {num_blocks} blocks of {num_destinations} destinations",
276 buf.len()
277 ))
278 })?;
279 let mut cumulative =
280 Vec::with_capacity((num_blocks * u64::from(num_destinations)) as usize);
281 for chunk in buf[COUNTS_HEADER_BYTES..].as_chunks::<4>().0 {
282 cumulative.push(u32::from_le_bytes(*chunk));
283 }
284 Ok(Self {
285 num_destinations,
286 block_rows,
287 total_rows,
288 cumulative,
289 })
290 }
291
292 fn check_shape(num_destinations: u32, block_rows: u32) -> Result<()> {
293 if num_destinations == 0 || num_destinations > MAX_DESTINATIONS {
294 return Err(Error::invalid_input(format!(
295 "stable partition needs 1..={MAX_DESTINATIONS} destinations, got {num_destinations}"
296 )));
297 }
298 if block_rows == 0 {
299 return Err(Error::invalid_input(
300 "stable partition block_rows must be positive",
301 ));
302 }
303 Ok(())
304 }
305
306 fn check_label(&self, label: u16) -> Result<()> {
307 if u32::from(label) >= self.num_destinations {
308 return Err(Error::invalid_input(format!(
309 "label {label} is outside the {} destinations of this stable partition",
310 self.num_destinations
311 )));
312 }
313 Ok(())
314 }
315}
316
317impl DeepSizeOf for CountsMatrix {
318 fn deep_size_of_children(&self, _: &mut Context) -> usize {
319 self.cumulative.capacity() * std::mem::size_of::<u32>()
320 }
321}
322
323#[derive(Debug)]
326pub struct CountsMatrixBuilder {
327 num_destinations: u32,
328 block_rows: u32,
329 running: Vec<u32>,
330 cumulative: Vec<u32>,
331 total_rows: u64,
332 rows_in_block: u32,
333}
334
335impl CountsMatrixBuilder {
336 pub fn try_new(num_destinations: u32, block_rows: u32) -> Result<Self> {
337 CountsMatrix::check_shape(num_destinations, block_rows)?;
338 Ok(Self {
339 num_destinations,
340 block_rows,
341 running: vec![0; num_destinations as usize],
342 cumulative: Vec::new(),
343 total_rows: 0,
344 rows_in_block: 0,
345 })
346 }
347
348 pub fn push(&mut self, label: Option<u16>) -> Result<()> {
351 if let Some(label) = label {
352 if u32::from(label) >= self.num_destinations {
353 return Err(Error::invalid_input(format!(
354 "label {label} is outside the {} destinations of this stable partition",
355 self.num_destinations
356 )));
357 }
358 let counter = &mut self.running[label as usize];
359 *counter = counter.checked_add(1).ok_or_else(|| {
360 Error::invalid_input(format!(
361 "destination {label} exceeds the u32 row-offset range"
362 ))
363 })?;
364 }
365 self.total_rows += 1;
366 self.rows_in_block += 1;
367 if self.rows_in_block == self.block_rows {
368 self.cumulative.extend_from_slice(&self.running);
369 self.rows_in_block = 0;
370 }
371 Ok(())
372 }
373
374 pub fn finish(mut self) -> CountsMatrix {
375 if self.rows_in_block > 0 {
376 self.cumulative.extend_from_slice(&self.running);
377 }
378 CountsMatrix {
379 num_destinations: self.num_destinations,
380 block_rows: self.block_rows,
381 total_rows: self.total_rows,
382 cumulative: self.cumulative,
383 }
384 }
385}
386
387pub fn rank_label_prefix(
390 values: &[u16],
391 validity: Option<&NullBuffer>,
392 upto: usize,
393 label: u16,
394) -> u32 {
395 match validity {
396 Some(validity) if validity.null_count() > 0 => values[..upto]
399 .iter()
400 .enumerate()
401 .filter(|&(i, &value)| value == label && validity.is_valid(i))
402 .count() as u32,
403 _ => values[..upto]
404 .iter()
405 .filter(|&&value| value == label)
406 .count() as u32,
407 }
408}
409
410pub fn translate_in_block(
417 counts: &CountsMatrix,
418 block: usize,
419 values: &[u16],
420 validity: Option<&NullBuffer>,
421 pos_in_block: usize,
422) -> Result<Option<(u16, u32)>> {
423 let block_range = counts.block_range(block);
424 let block_len = (block_range.end - block_range.start) as usize;
425 if values.len() != block_len {
426 return Err(Error::invalid_input(format!(
427 "block {block} holds {block_len} labels but {} were supplied",
428 values.len()
429 )));
430 }
431 if pos_in_block >= block_len {
432 return Err(Error::invalid_input(format!(
433 "row {pos_in_block} is outside block {block} of {block_len} rows"
434 )));
435 }
436 if validity.is_some_and(|validity| !validity.is_valid(pos_in_block)) {
437 return Ok(None);
438 }
439 let label = values[pos_in_block];
440 counts.check_label(label)?;
441 let offset = counts.count_before(block, label)
442 + rank_label_prefix(values, validity, pos_in_block, label);
443 Ok(Some((label, offset)))
444}
445
446#[derive(Debug)]
449pub struct SweepTranslator {
450 counters: Vec<u32>,
451}
452
453impl SweepTranslator {
454 pub fn new(counts: &CountsMatrix, start_block: usize) -> Result<Self> {
456 if start_block >= counts.num_blocks().max(1) {
457 return Err(Error::invalid_input(format!(
458 "start block {start_block} is outside the {} blocks of this stable partition",
459 counts.num_blocks()
460 )));
461 }
462 Ok(Self {
463 counters: counts.counters_at_block(start_block),
464 })
465 }
466
467 #[inline]
469 pub fn advance(&mut self, label: Option<u16>) -> Result<Option<(u16, u32)>> {
470 let Some(label) = label else {
471 return Ok(None);
472 };
473 let counter = self.counters.get_mut(label as usize).ok_or_else(|| {
474 Error::invalid_input(format!(
475 "label {label} is outside the destinations of this stable partition"
476 ))
477 })?;
478 let offset = *counter;
479 *counter += 1;
480 Ok(Some((label, offset)))
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use rand::rngs::StdRng;
488 use rand::{Rng, SeedableRng};
489
490 fn random_labels(rng: &mut StdRng, rows: usize, m: u32) -> Vec<Option<u16>> {
492 (0..rows)
493 .map(|_| {
494 if rng.random_ratio(1, 8) {
495 None
496 } else {
497 Some(rng.random_range(0..m) as u16)
498 }
499 })
500 .collect()
501 }
502
503 fn build(labels: &[Option<u16>], m: u32, block_rows: u32) -> CountsMatrix {
504 let mut builder = CountsMatrixBuilder::try_new(m, block_rows).unwrap();
505 for &label in labels {
506 builder.push(label).unwrap();
507 }
508 builder.finish()
509 }
510
511 fn reference(labels: &[Option<u16>]) -> Vec<Option<(u16, u32)>> {
513 let mut counters = std::collections::HashMap::new();
514 labels
515 .iter()
516 .map(|label| {
517 label.map(|label| {
518 let counter = counters.entry(label).or_insert(0u32);
519 let offset = *counter;
520 *counter += 1;
521 (label, offset)
522 })
523 })
524 .collect()
525 }
526
527 fn block_slices(
528 labels: &[Option<u16>],
529 counts: &CountsMatrix,
530 block: usize,
531 ) -> (Vec<u16>, Option<NullBuffer>) {
532 let range = counts.block_range(block);
533 let block_labels = &labels[range.start as usize..range.end as usize];
534 let values = block_labels
537 .iter()
538 .map(|label| label.unwrap_or(0xBEEF_u64 as u16))
539 .collect();
540 let has_nulls = block_labels.iter().any(Option::is_none);
541 let validity =
542 has_nulls.then(|| NullBuffer::from_iter(block_labels.iter().map(Option::is_some)));
543 (values, validity)
544 }
545
546 #[test]
547 fn test_counts_matrix_hand_example() {
548 let labels = [
551 Some(0),
552 Some(2),
553 None,
554 Some(2), Some(1),
556 Some(1),
557 Some(0),
558 Some(2), None,
560 Some(0), ];
562 let counts = build(&labels, 3, 4);
563 counts.validate().unwrap();
564 assert_eq!(counts.num_blocks(), 3);
565 assert_eq!(counts.total_rows(), 10);
566 assert_eq!(counts.block_range(2), 8..10);
567
568 assert_eq!(counts.count_before(0, 2), 0);
569 assert_eq!(counts.count_before(1, 2), 2);
570 assert_eq!(counts.count_before(2, 0), 2);
571 assert_eq!(counts.count_before(1, 1), 0);
574 assert_eq!(counts.count_before(3, 1), 2);
575 assert_eq!(
576 (counts.total(0), counts.total(1), counts.total(2)),
577 (3, 2, 3)
578 );
579 assert_eq!(counts.total_live_rows(), 8);
580 assert_eq!(counts.deleted_through(0), 1);
581 assert_eq!(counts.deleted_through(2), 2);
582 assert_eq!(counts.counters_at_block(1), vec![1, 0, 2]);
583
584 let exact = build(&labels[..8], 3, 4);
586 assert_eq!(exact.num_blocks(), 2);
587 assert_eq!(exact.total_rows(), 8);
588
589 let empty = build(&[], 3, 4);
590 assert_eq!(empty.num_blocks(), 0);
591 assert_eq!(empty.total(1), 0);
592 assert_eq!(empty.total_live_rows(), 0);
593 empty.validate().unwrap();
594 }
595
596 #[test]
597 fn test_encode_decode_round_trip() {
598 let mut rng = StdRng::seed_from_u64(7);
599 let labels = random_labels(&mut rng, 1000, 5);
600 let counts = build(&labels, 5, 64);
601 let decoded = CountsMatrix::decode(&counts.encode()).unwrap();
602 assert_eq!(decoded, counts);
603 decoded.validate().unwrap();
604
605 let empty = build(&[], 5, 64);
606 assert_eq!(CountsMatrix::decode(&empty.encode()).unwrap(), empty);
607
608 assert!(CountsMatrix::decode(&[]).is_err());
611 let mut bad_magic = counts.encode();
612 bad_magic[0] = b'X';
613 assert!(CountsMatrix::decode(&bad_magic).is_err());
614 let mut truncated = counts.encode();
615 truncated.pop();
616 assert!(CountsMatrix::decode(&truncated).is_err());
617 let mut bad_repr = counts.encode();
618 bad_repr[8] = 9;
619 assert!(CountsMatrix::decode(&bad_repr).is_err());
620
621 let mut decreasing = counts;
624 let m = decreasing.num_destinations as usize;
625 decreasing.cumulative[m] = 0;
626 assert!(
627 CountsMatrix::decode(&decreasing.encode())
628 .unwrap()
629 .validate()
630 .is_err()
631 );
632 }
633
634 #[test]
635 fn test_builder_rejects_bad_shapes_and_labels() {
636 assert!(CountsMatrixBuilder::try_new(0, 64).is_err());
637 assert!(CountsMatrixBuilder::try_new(MAX_DESTINATIONS + 1, 64).is_err());
638 assert!(CountsMatrixBuilder::try_new(1, 0).is_err());
639 let mut builder = CountsMatrixBuilder::try_new(3, 4).unwrap();
640 assert!(builder.push(Some(3)).is_err());
641 assert!(builder.push(Some(2)).is_ok());
642 }
643
644 #[test]
645 fn test_point_and_sweep_match_reference() {
646 let m = 5u32;
647 let mut rng = StdRng::seed_from_u64(42);
648 let labels = random_labels(&mut rng, 1000, m);
649 let counts = build(&labels, m, 64);
650 counts.validate().unwrap();
651 let expected = reference(&labels);
652
653 for block in 0..counts.num_blocks() {
655 let (values, validity) = block_slices(&labels, &counts, block);
656 let range = counts.block_range(block);
657 for pos in 0..(range.end - range.start) as usize {
658 let translated =
659 translate_in_block(&counts, block, &values, validity.as_ref(), pos).unwrap();
660 assert_eq!(
661 translated,
662 expected[range.start as usize + pos],
663 "row {}",
664 range.start as usize + pos
665 );
666 }
667 }
668
669 for start_block in 0..counts.num_blocks() {
671 let mut sweep = SweepTranslator::new(&counts, start_block).unwrap();
672 let start_row = counts.block_range(start_block).start as usize;
673 for (row, &label) in labels.iter().enumerate().skip(start_row) {
674 assert_eq!(
675 sweep.advance(label).unwrap(),
676 expected[row],
677 "row {row} from block {start_block}"
678 );
679 }
680 }
681 }
682
683 #[test]
684 fn test_translate_in_block_input_checks() {
685 let labels = [Some(0), Some(1), None, Some(0)];
686 let counts = build(&labels, 2, 4);
687 let (values, validity) = block_slices(&labels, &counts, 0);
688 assert!(translate_in_block(&counts, 0, &values[..3], None, 0).is_err());
690 assert!(translate_in_block(&counts, 0, &values, validity.as_ref(), 4).is_err());
691 assert!(translate_in_block(&counts, 0, &[0, 9, 0, 0], None, 1).is_err());
693 assert!(SweepTranslator::new(&counts, 1).is_err());
694 let mut sweep = SweepTranslator::new(&counts, 0).unwrap();
695 assert!(sweep.advance(Some(9)).is_err());
696 }
697}