1#[cfg(feature = "native")]
12use rayon::prelude::*;
13use rustc_hash::FxHashMap;
14
15pub(crate) struct ForwardIndex {
21 terms: Vec<u32>,
22 offsets: Vec<u32>,
23 pub num_terms: usize,
24}
25
26impl ForwardIndex {
27 #[inline]
28 pub fn num_docs(&self) -> usize {
29 if self.offsets.is_empty() {
30 0
31 } else {
32 self.offsets.len() - 1
33 }
34 }
35
36 #[inline]
37 fn doc_terms(&self, doc: usize) -> &[u32] {
38 let start = self.offsets[doc] as usize;
39 let end = self.offsets[doc + 1] as usize;
40 &self.terms[start..end]
41 }
42
43 pub fn total_postings(&self) -> u32 {
45 self.offsets.last().copied().unwrap_or(0)
46 }
47}
48
49pub(crate) fn build_vid_maps(bmp: &crate::segment::reader::bmp::BmpIndex) -> (Vec<u32>, Vec<u32>) {
59 let ids = bmp.doc_map_ids_slice();
60 let num_virtual = bmp.num_virtual_docs as usize;
61 let mut virtual_to_real = vec![u32::MAX; num_virtual];
62 let mut real_to_virtual = Vec::with_capacity(bmp.num_real_docs() as usize);
63 for (vid, (slot, chunk)) in virtual_to_real
64 .iter_mut()
65 .zip(ids.as_chunks::<4>().0)
66 .enumerate()
67 {
68 let doc_id = u32::from_le_bytes(*chunk);
69 if doc_id != u32::MAX {
70 *slot = real_to_virtual.len() as u32;
71 real_to_virtual.push(vid as u32);
72 }
73 }
74 if real_to_virtual.len() != bmp.num_real_docs() as usize {
75 log::warn!(
76 "[reorder] BMP doc map has {} real slots but footer says num_real_docs={} — trusting the doc map",
77 real_to_virtual.len(),
78 bmp.num_real_docs(),
79 );
80 }
81 (virtual_to_real, real_to_virtual)
82}
83
84pub(crate) fn build_forward_index_from_bmps(
98 bmps: &[&crate::segment::reader::bmp::BmpIndex],
99 min_doc_freq: usize,
100 max_doc_freq: usize,
101 memory_budget_bytes: usize,
102) -> (ForwardIndex, Vec<usize>) {
103 let vid_maps: Vec<(Vec<u32>, Vec<u32>)> = bmps.iter().map(|b| build_vid_maps(b)).collect();
105 let source_doc_counts: Vec<usize> = vid_maps.iter().map(|(_, r2v)| r2v.len()).collect();
106 let total_docs: usize = source_doc_counts.iter().sum();
107
108 if total_docs == 0 {
109 return (
110 ForwardIndex {
111 terms: Vec::new(),
112 offsets: Vec::new(),
113 num_terms: 0,
114 },
115 source_doc_counts,
116 );
117 }
118
119 let mut dim_df: FxHashMap<u32, usize> = FxHashMap::default();
121 for (bmp, (v2r, _)) in bmps.iter().zip(&vid_maps) {
122 let num_blocks = bmp.num_blocks as usize;
123 let block_size = bmp.bmp_block_size as usize;
124 for block_id in 0..num_blocks {
125 for (dim_id, postings) in bmp.iter_block_terms(block_id as u32) {
126 for p in postings {
127 let vid = block_id * block_size + p.local_slot as usize;
128 if v2r[vid] != u32::MAX && p.impact > 0 {
129 *dim_df.entry(dim_id).or_insert(0) += 1;
130 }
131 }
132 }
133 }
134 }
135
136 let mut eligible: Vec<(u32, usize)> = dim_df
138 .iter()
139 .filter(|&(_, df)| *df >= min_doc_freq && *df <= max_doc_freq)
140 .map(|(&dim_id, &df)| (dim_id, df))
141 .collect();
142 drop(dim_df);
143
144 let total_postings_est: usize = eligible.iter().map(|(_, df)| *df).sum();
148 let estimated_bytes = total_postings_est * 4 + total_docs * 28 + eligible.len() * 8;
149
150 if estimated_bytes > memory_budget_bytes && !eligible.is_empty() {
151 eligible.sort_by_key(|&(_, df)| df);
154
155 let target_postings =
156 memory_budget_bytes.saturating_sub(total_docs * 28 + eligible.len() * 8) / 4;
157 let mut cum = 0usize;
158 let mut keep_count = 0;
159 for &(_, df) in &eligible {
160 if cum + df > target_postings {
161 break;
162 }
163 cum += df;
164 keep_count += 1;
165 }
166
167 let dropped = eligible.len() - keep_count;
168 eligible.truncate(keep_count);
169
170 log::warn!(
171 "[reorder] memory budget {:.0} MB: estimated {:.0} MB, dropped {} highest-df dims, keeping {} ({} postings)",
172 memory_budget_bytes as f64 / (1024.0 * 1024.0),
173 estimated_bytes as f64 / (1024.0 * 1024.0),
174 dropped,
175 keep_count,
176 cum,
177 );
178 }
179
180 let mut term_remap: FxHashMap<u32, u32> = FxHashMap::default();
181 for &(dim_id, _) in &eligible {
182 let compact_id = term_remap.len() as u32;
183 term_remap.insert(dim_id, compact_id);
184 }
185 let num_active_terms = term_remap.len();
186 drop(eligible);
187
188 let mut counts = vec![0u32; total_docs];
190 let mut real_offset = 0usize;
191
192 for (src_idx, bmp) in bmps.iter().enumerate() {
193 let (v2r, _) = &vid_maps[src_idx];
194 let num_blocks = bmp.num_blocks as usize;
195 let block_size = bmp.bmp_block_size as usize;
196 for block_id in 0..num_blocks {
197 for (dim_id, postings) in bmp.iter_block_terms(block_id as u32) {
198 if !term_remap.contains_key(&dim_id) {
199 continue;
200 }
201 for p in postings {
202 let vid = block_id * block_size + p.local_slot as usize;
203 let real = v2r[vid];
204 if real != u32::MAX && p.impact > 0 {
205 counts[real_offset + real as usize] += 1;
206 }
207 }
208 }
209 }
210 real_offset += source_doc_counts[src_idx];
211 }
212
213 let mut offsets = Vec::with_capacity(total_docs + 1);
215 offsets.push(0u32);
216 for &c in &counts {
217 offsets.push(offsets.last().unwrap() + c);
218 }
219 let total = *offsets.last().unwrap() as usize;
220
221 let mut terms = vec![0u32; total];
223 counts.fill(0);
224 real_offset = 0;
225
226 for (src_idx, bmp) in bmps.iter().enumerate() {
227 let (v2r, _) = &vid_maps[src_idx];
228 let num_blocks = bmp.num_blocks as usize;
229 let block_size = bmp.bmp_block_size as usize;
230 for block_id in 0..num_blocks {
231 for (dim_id, postings) in bmp.iter_block_terms(block_id as u32) {
232 let Some(&compact) = term_remap.get(&dim_id) else {
233 continue;
234 };
235 for p in postings {
236 let vid = block_id * block_size + p.local_slot as usize;
237 let real = v2r[vid];
238 if real != u32::MAX && p.impact > 0 {
239 let global_real = real_offset + real as usize;
240 let pos = offsets[global_real] as usize + counts[global_real] as usize;
241 terms[pos] = compact;
242 counts[global_real] += 1;
243 }
244 }
245 }
246 }
247 real_offset += source_doc_counts[src_idx];
248 }
249
250 (
251 ForwardIndex {
252 terms,
253 offsets,
254 num_terms: num_active_terms,
255 },
256 source_doc_counts,
257 )
258}
259
260pub(crate) fn build_forward_index_from_blocks(
268 bmps: &[&crate::segment::reader::bmp::BmpIndex],
269 memory_budget_bytes: usize,
270) -> ForwardIndex {
271 let total_blocks: usize = bmps.iter().map(|b| b.num_blocks as usize).sum();
272 if total_blocks == 0 {
273 return ForwardIndex {
274 terms: Vec::new(),
275 offsets: Vec::new(),
276 num_terms: 0,
277 };
278 }
279
280 let mut dim_bf: FxHashMap<u32, usize> = FxHashMap::default();
282 for bmp in bmps {
283 for block_id in 0..bmp.num_blocks {
284 for (dim_id, _) in bmp.iter_block_terms(block_id) {
285 *dim_bf.entry(dim_id).or_insert(0) += 1;
286 }
287 }
288 }
289
290 let max_bf = (total_blocks as f64 * 0.9) as usize;
291 let mut eligible: Vec<(u32, usize)> = dim_bf
292 .iter()
293 .filter(|&(_, bf)| *bf >= 2 && *bf <= max_bf.max(2))
294 .map(|(&dim_id, &bf)| (dim_id, bf))
295 .collect();
296 drop(dim_bf);
297
298 let total_postings_est: usize = eligible.iter().map(|(_, bf)| *bf).sum();
299 let estimated_bytes = total_postings_est * 4 + total_blocks * 28 + eligible.len() * 8;
300 if estimated_bytes > memory_budget_bytes && !eligible.is_empty() {
301 eligible.sort_by_key(|&(_, bf)| bf);
302 let target = memory_budget_bytes.saturating_sub(total_blocks * 28 + eligible.len() * 8) / 4;
303 let mut cum = 0usize;
304 let mut keep = 0;
305 for &(_, bf) in &eligible {
306 if cum + bf > target {
307 break;
308 }
309 cum += bf;
310 keep += 1;
311 }
312 log::warn!(
313 "[reorder] block-level fwd index over budget — dropped {} highest-bf dims",
314 eligible.len() - keep,
315 );
316 eligible.truncate(keep);
317 }
318
319 let mut term_remap: FxHashMap<u32, u32> = FxHashMap::default();
320 for &(dim_id, _) in &eligible {
321 let compact = term_remap.len() as u32;
322 term_remap.insert(dim_id, compact);
323 }
324 let num_terms = term_remap.len();
325 drop(eligible);
326
327 let mut counts = vec![0u32; total_blocks];
329 let mut gb = 0usize;
330 for bmp in bmps {
331 for block_id in 0..bmp.num_blocks {
332 for (dim_id, _) in bmp.iter_block_terms(block_id) {
333 if term_remap.contains_key(&dim_id) {
334 counts[gb] += 1;
335 }
336 }
337 gb += 1;
338 }
339 }
340 let mut offsets = Vec::with_capacity(total_blocks + 1);
341 offsets.push(0u32);
342 for &c in &counts {
343 offsets.push(offsets.last().unwrap() + c);
344 }
345 let total = *offsets.last().unwrap() as usize;
346 let mut terms = vec![0u32; total];
347 counts.fill(0);
348 gb = 0;
349 for bmp in bmps {
350 for block_id in 0..bmp.num_blocks {
351 for (dim_id, _) in bmp.iter_block_terms(block_id) {
352 if let Some(&compact) = term_remap.get(&dim_id) {
353 let pos = offsets[gb] as usize + counts[gb] as usize;
354 terms[pos] = compact;
355 counts[gb] += 1;
356 }
357 }
358 gb += 1;
359 }
360 }
361
362 ForwardIndex {
363 terms,
364 offsets,
365 num_terms,
366 }
367}
368
369#[derive(Clone, Copy, Debug, Default)]
377pub struct BpBudget {
378 pub min_partition_docs: Option<usize>,
383 pub time_budget: Option<std::time::Duration>,
387}
388
389impl BpBudget {
390 pub fn full() -> Self {
392 Self::default()
393 }
394}
395
396pub(crate) fn graph_bisection(
407 fwd: &ForwardIndex,
408 min_partition_size: usize,
409 max_iters: usize,
410 budget: BpBudget,
411) -> (Vec<u32>, bool) {
412 let n = fwd.num_docs();
413 if n == 0 {
414 return (Vec::new(), true);
415 }
416
417 let effective_min_partition = budget
418 .min_partition_docs
419 .unwrap_or(0)
420 .max(min_partition_size);
421
422 let mut docs: Vec<u32> = (0..n as u32).collect();
423 let depth = if effective_min_partition > 0 {
424 ((n as f64) / (effective_min_partition as f64))
425 .log2()
426 .ceil() as usize
427 } else {
428 0
429 };
430 let log_table = build_log_table(4096);
431
432 log::debug!(
433 "BP graph_bisection: n={}, min_partition={}, max_iters={}, depth=~{}, time_budget={:?}",
434 n,
435 effective_min_partition,
436 max_iters,
437 depth,
438 budget.time_budget,
439 );
440
441 #[cfg(feature = "native")]
442 let deadline = budget.time_budget.map(|d| std::time::Instant::now() + d);
443 #[cfg(not(feature = "native"))]
444 let deadline: Option<()> = None;
445 #[cfg(not(feature = "native"))]
446 let _ = deadline;
447
448 let exhausted = std::sync::atomic::AtomicBool::new(false);
449 #[cfg(feature = "native")]
450 bisect(
451 &mut docs,
452 fwd,
453 effective_min_partition,
454 max_iters,
455 &log_table,
456 deadline,
457 &exhausted,
458 );
459 #[cfg(not(feature = "native"))]
460 bisect(
461 &mut docs,
462 fwd,
463 effective_min_partition,
464 max_iters,
465 &log_table,
466 None,
467 &exhausted,
468 );
469
470 let converged = !exhausted.load(std::sync::atomic::Ordering::Relaxed);
471 if !converged {
472 log::info!(
473 "BP graph_bisection: wall-clock budget {:?} exhausted at n={} — emitting partial (still valid) permutation",
474 budget.time_budget,
475 n,
476 );
477 }
478 (docs, converged)
479}
480
481fn bisect(
490 docs: &mut [u32],
491 fwd: &ForwardIndex,
492 min_partition_size: usize,
493 max_iters: usize,
494 log_table: &[f32],
495 #[cfg(feature = "native")] deadline: Option<std::time::Instant>,
496 #[cfg(not(feature = "native"))] deadline: Option<()>,
497 exhausted: &std::sync::atomic::AtomicBool,
498) {
499 let n = docs.len();
500 if n <= min_partition_size {
501 return;
502 }
503 if exhausted.load(std::sync::atomic::Ordering::Relaxed) {
505 return;
506 }
507 #[cfg(feature = "native")]
508 if let Some(dl) = deadline
509 && std::time::Instant::now() >= dl
510 {
511 exhausted.store(true, std::sync::atomic::Ordering::Relaxed);
512 return;
513 }
514 #[cfg(not(feature = "native"))]
515 let _ = deadline;
516
517 let mid = n / 2;
518 let nt = fwd.num_terms;
519
520 let effective_iters = if n > 100_000 {
524 max_iters.min(12)
525 } else {
526 max_iters
527 };
528
529 let mut left_deg = vec![0u32; nt];
533 let mut right_deg = vec![0u32; nt];
534
535 for (i, &doc) in docs.iter().enumerate() {
536 let target = if i < mid {
537 &mut left_deg
538 } else {
539 &mut right_deg
540 };
541 for &term in fwd.doc_terms(doc as usize) {
542 target[term as usize] += 1;
543 }
544 }
545
546 let mut gains: Vec<f32> = vec![0.0; n];
548 let mut indices: Vec<usize> = (0..n).collect();
549 let mut new_left: Vec<u32> = Vec::with_capacity(mid);
550 let mut new_right: Vec<u32> = Vec::with_capacity(n - mid);
551
552 for iter in 0..effective_iters {
553 #[cfg(feature = "native")]
555 if let Some(dl) = deadline
556 && std::time::Instant::now() >= dl
557 {
558 exhausted.store(true, std::sync::atomic::Ordering::Relaxed);
559 break;
560 }
561 compute_gains(docs, fwd, mid, &left_deg, &right_deg, log_table, &mut gains);
564
565 indices.clear();
567 indices.extend(0..n);
568 indices.select_nth_unstable_by(mid, |&a, &b| {
569 gains[a]
570 .partial_cmp(&gains[b])
571 .unwrap_or(std::cmp::Ordering::Equal)
572 });
573
574 new_left.clear();
576 new_right.clear();
577 let mut swap_count: usize = 0;
578
579 for (rank, &idx) in indices.iter().enumerate() {
580 let doc = docs[idx];
581 let was_left = idx < mid;
582 let now_left = rank < mid;
583
584 if now_left {
585 new_left.push(doc);
586 } else {
587 new_right.push(doc);
588 }
589
590 if was_left != now_left {
591 swap_count += 1;
592 for &term in fwd.doc_terms(doc as usize) {
593 let t = term as usize;
594 if was_left {
595 left_deg[t] -= 1;
596 right_deg[t] += 1;
597 } else {
598 right_deg[t] -= 1;
599 left_deg[t] += 1;
600 }
601 }
602 }
603 }
604
605 docs[..mid].copy_from_slice(&new_left);
606 docs[mid..].copy_from_slice(&new_right);
607
608 if swap_count == 0 {
609 break;
610 }
611
612 if iter > 2 && swap_count < n / 200 {
614 break;
615 }
616
617 if iter > 5 {
619 let max_gain = gains.iter().copied().fold(f32::NEG_INFINITY, f32::max);
620 if max_gain.abs() < 0.001 {
621 break;
622 }
623 }
624 }
625
626 drop(left_deg);
628 drop(right_deg);
629 drop(gains);
630 drop(indices);
631 drop(new_left);
632 drop(new_right);
633
634 let (left, right) = docs.split_at_mut(mid);
635 #[cfg(feature = "native")]
636 rayon::join(
637 || {
638 bisect(
639 left,
640 fwd,
641 min_partition_size,
642 max_iters,
643 log_table,
644 deadline,
645 exhausted,
646 )
647 },
648 || {
649 bisect(
650 right,
651 fwd,
652 min_partition_size,
653 max_iters,
654 log_table,
655 deadline,
656 exhausted,
657 )
658 },
659 );
660 #[cfg(not(feature = "native"))]
661 {
662 bisect(
663 left,
664 fwd,
665 min_partition_size,
666 max_iters,
667 log_table,
668 deadline,
669 exhausted,
670 );
671 bisect(
672 right,
673 fwd,
674 min_partition_size,
675 max_iters,
676 log_table,
677 deadline,
678 exhausted,
679 );
680 }
681}
682
683#[inline(never)]
689fn compute_gains(
690 docs: &[u32],
691 fwd: &ForwardIndex,
692 mid: usize,
693 left_deg: &[u32],
694 right_deg: &[u32],
695 log_table: &[f32],
696 gains: &mut [f32],
697) {
698 let gain_for_doc = |i: usize| -> f32 {
707 let doc = docs[i] as usize;
708 let in_left = i < mid;
709 let mut g = 0.0f32;
710 for &term in fwd.doc_terms(doc) {
711 let t = term as usize;
712 let (from, to) = if in_left {
713 (left_deg[t], right_deg[t])
714 } else {
715 (right_deg[t], left_deg[t])
716 };
717 let move_gain = fast_log2_lookup(to as usize + 2, log_table)
718 - fast_log2_lookup(from as usize, log_table)
719 - std::f32::consts::LOG2_E / (1.0 + to as f32);
720 g += if in_left { move_gain } else { -move_gain };
721 }
722 g
723 };
724
725 #[cfg(feature = "native")]
726 {
727 if docs.len() > 4096 {
728 gains
729 .par_iter_mut()
730 .enumerate()
731 .for_each(|(i, gain)| *gain = gain_for_doc(i));
732 } else {
733 for (i, gain) in gains.iter_mut().enumerate().take(docs.len()) {
734 *gain = gain_for_doc(i);
735 }
736 }
737 }
738 #[cfg(not(feature = "native"))]
739 {
740 for (i, gain) in gains.iter_mut().enumerate().take(docs.len()) {
741 *gain = gain_for_doc(i);
742 }
743 }
744}
745
746fn build_log_table(size: usize) -> Vec<f32> {
750 let mut table = vec![0.0f32; size];
751 table[0] = -10.0;
753 for (i, entry) in table.iter_mut().enumerate().skip(1) {
754 *entry = (i as f32).log2();
755 }
756 table
757}
758
759#[inline]
761fn fast_log2_lookup(val: usize, table: &[f32]) -> f32 {
762 if val < table.len() {
763 table[val]
764 } else {
765 (val as f32).log2()
766 }
767}
768
769#[cfg(test)]
770mod tests {
771 use super::*;
772
773 fn make_fwd(docs: &[&[u32]], num_terms: usize) -> ForwardIndex {
775 let mut terms = Vec::new();
776 let mut offsets = vec![0u32];
777 for doc_terms in docs {
778 terms.extend_from_slice(doc_terms);
779 offsets.push(terms.len() as u32);
780 }
781 ForwardIndex {
782 terms,
783 offsets,
784 num_terms,
785 }
786 }
787
788 #[test]
789 fn test_bp_empty() {
790 let fwd = ForwardIndex {
791 terms: Vec::new(),
792 offsets: Vec::new(),
793 num_terms: 0,
794 };
795 let (perm, _) = graph_bisection(&fwd, 4, 20, BpBudget::full());
796 assert!(perm.is_empty());
797 }
798
799 #[test]
800 fn test_bp_small() {
801 let fwd = make_fwd(&[&[0, 1], &[0, 2], &[1, 3], &[2, 3]], 4);
803 let (perm, _) = graph_bisection(&fwd, 4, 20, BpBudget::full());
804 assert_eq!(perm.len(), 4);
805 let mut sorted = perm.clone();
807 sorted.sort();
808 assert_eq!(sorted, vec![0, 1, 2, 3]);
809 }
810
811 #[test]
812 fn test_bp_clusters() {
813 let fwd = make_fwd(
817 &[
818 &[0, 1],
819 &[0, 1],
820 &[0, 1],
821 &[0, 1],
822 &[2, 3],
823 &[2, 3],
824 &[2, 3],
825 &[2, 3],
826 ],
827 4,
828 );
829 let (perm, _) = graph_bisection(&fwd, 4, 20, BpBudget::full());
830 assert_eq!(perm.len(), 8);
831
832 let left: Vec<u32> = perm[..4].to_vec();
834
835 let a_in_left = left.iter().filter(|&&d| d < 4).count();
837 let b_in_left = left.iter().filter(|&&d| d >= 4).count();
838 assert!(
839 (a_in_left == 4 && b_in_left == 0) || (a_in_left == 0 && b_in_left == 4),
840 "Clusters should be separated: a_left={}, b_left={}",
841 a_in_left,
842 b_in_left,
843 );
844 }
845
846 #[test]
847 fn test_bp_permutation_valid() {
848 let docs: Vec<Vec<u32>> = (0..16).map(|i| vec![i / 4, 10 + i / 2]).collect();
850 let doc_refs: Vec<&[u32]> = docs.iter().map(|v| v.as_slice()).collect();
851 let fwd = make_fwd(&doc_refs, 18); let (perm, _) = graph_bisection(&fwd, 4, 20, BpBudget::full());
853
854 assert_eq!(perm.len(), 16);
855 let mut sorted = perm.clone();
857 sorted.sort();
858 let expected: Vec<u32> = (0..16).collect();
859 assert_eq!(sorted, expected);
860 }
861
862 #[test]
867 fn test_bp_depth_cap_separates_clusters_and_converges() {
868 let fwd = make_fwd(
871 &[
872 &[0, 1],
873 &[0, 1],
874 &[0, 1],
875 &[2, 3],
876 &[0, 1],
877 &[2, 3],
878 &[2, 3],
879 &[2, 3],
880 ],
881 4,
882 );
883 let budget = BpBudget {
884 min_partition_docs: Some(4),
885 time_budget: None,
886 };
887 let (perm, converged) = graph_bisection(&fwd, 2, 20, budget);
888 assert!(converged, "depth cap must report converged");
889 assert_eq!(perm.len(), 8);
890 let mut sorted = perm.clone();
891 sorted.sort();
892 assert_eq!(
893 sorted,
894 (0..8).collect::<Vec<u32>>(),
895 "must stay a valid permutation"
896 );
897 let cluster_a = [0u32, 1, 2, 4];
900 let a_in_left = perm[..4].iter().filter(|d| cluster_a.contains(d)).count();
901 assert!(
902 a_in_left == 4 || a_in_left == 0,
903 "clusters should separate at the top level: {:?}",
904 perm
905 );
906 }
907
908 #[test]
911 fn test_bp_zero_time_budget_emits_valid_partial_permutation() {
912 let docs: Vec<Vec<u32>> = (0..64).map(|i| vec![i % 4]).collect();
913 let doc_refs: Vec<&[u32]> = docs.iter().map(|v| v.as_slice()).collect();
914 let fwd = make_fwd(&doc_refs, 4);
915 let budget = BpBudget {
916 min_partition_docs: None,
917 time_budget: Some(std::time::Duration::ZERO),
918 };
919 let (perm, converged) = graph_bisection(&fwd, 4, 20, budget);
920 assert!(!converged, "zero budget must report unconverged");
921 assert_eq!(perm.len(), 64);
922 let mut sorted = perm.clone();
923 sorted.sort();
924 assert_eq!(sorted, (0..64).collect::<Vec<u32>>());
925 }
926
927 #[test]
928 fn test_fast_log2() {
929 let table = build_log_table(4096);
930 assert!((table[1] - 0.0).abs() < 0.001);
931 assert!((table[2] - 1.0).abs() < 0.001);
932 assert!((table[4] - 2.0).abs() < 0.001);
933 assert!((table[1024] - 10.0).abs() < 0.001);
934 let val = fast_log2_lookup(8192, &table);
936 assert!((val - 13.0).abs() < 0.001);
937 }
938}