1use rayon::prelude::*;
13
14#[derive(Debug, Clone)]
20pub struct Tile {
21 pub data: Vec<f64>,
23}
24
25impl Tile {
26 pub fn from_slice(s: &[f64]) -> Self {
28 Self { data: s.to_vec() }
29 }
30
31 pub fn reduce_sum(&self) -> f64 {
33 self.data.iter().copied().sum()
34 }
35
36 pub fn reduce_max(&self) -> f64 {
38 self.data.iter().copied().fold(f64::NEG_INFINITY, f64::max)
39 }
40
41 pub fn reduce_min(&self) -> f64 {
43 self.data.iter().copied().fold(f64::INFINITY, f64::min)
44 }
45
46 pub fn exclusive_scan_inplace(&mut self) {
48 let mut acc = 0.0;
49 for v in &mut self.data {
50 let old = *v;
51 *v = acc;
52 acc += old;
53 }
54 }
55
56 pub fn inclusive_scan_inplace(&mut self) {
58 let mut acc = 0.0;
59 for v in &mut self.data {
60 acc += *v;
61 *v = acc;
62 }
63 }
64}
65
66#[derive(Debug, Clone)]
75pub struct TiledReducer {
76 pub tile_size: usize,
78}
79
80impl TiledReducer {
81 pub fn new(tile_size: usize) -> Self {
83 assert!(tile_size > 0, "tile_size must be > 0");
84 Self { tile_size }
85 }
86
87 pub fn sum(&self, data: &[f64]) -> f64 {
89 if data.is_empty() {
90 return 0.0;
91 }
92 let tile_sums: Vec<f64> = data
93 .par_chunks(self.tile_size)
94 .map(|chunk| chunk.iter().copied().sum::<f64>())
95 .collect();
96 tile_sums.iter().copied().sum()
97 }
98
99 pub fn max(&self, data: &[f64]) -> f64 {
101 if data.is_empty() {
102 return f64::NEG_INFINITY;
103 }
104 let tile_maxs: Vec<f64> = data
105 .par_chunks(self.tile_size)
106 .map(|chunk| chunk.iter().copied().fold(f64::NEG_INFINITY, f64::max))
107 .collect();
108 tile_maxs.iter().copied().fold(f64::NEG_INFINITY, f64::max)
109 }
110
111 pub fn min(&self, data: &[f64]) -> f64 {
113 if data.is_empty() {
114 return f64::INFINITY;
115 }
116 let tile_mins: Vec<f64> = data
117 .par_chunks(self.tile_size)
118 .map(|chunk| chunk.iter().copied().fold(f64::INFINITY, f64::min))
119 .collect();
120 tile_mins.iter().copied().fold(f64::INFINITY, f64::min)
121 }
122
123 pub fn dot(&self, a: &[f64], b: &[f64]) -> f64 {
125 assert_eq!(a.len(), b.len(), "dot product requires equal-length inputs");
126 a.par_iter()
127 .zip(b.par_iter())
128 .map(|(&ai, &bi)| ai * bi)
129 .sum()
130 }
131
132 pub fn tile_sums(&self, data: &[f64]) -> Vec<f64> {
134 data.par_chunks(self.tile_size)
135 .map(|chunk| chunk.iter().copied().sum::<f64>())
136 .collect()
137 }
138}
139
140pub fn segmented_exclusive_scan(data: &[f64], flags: &[bool]) -> Vec<f64> {
149 assert_eq!(
150 data.len(),
151 flags.len(),
152 "data and flags must be same length"
153 );
154 let mut result = vec![0.0; data.len()];
155 let mut acc = 0.0;
156 for i in 0..data.len() {
157 if flags[i] {
158 acc = 0.0; }
160 result[i] = acc;
161 acc += data[i];
162 }
163 result
164}
165
166pub fn segmented_inclusive_scan(data: &[f64], flags: &[bool]) -> Vec<f64> {
168 assert_eq!(data.len(), flags.len());
169 let mut result = vec![0.0; data.len()];
170 let mut acc = 0.0;
171 for i in 0..data.len() {
172 if flags[i] {
173 acc = 0.0;
174 }
175 acc += data[i];
176 result[i] = acc;
177 }
178 result
179}
180
181pub fn segmented_reduce_sum(data: &[f64], flags: &[bool]) -> Vec<f64> {
183 assert_eq!(data.len(), flags.len());
184 let mut sums: Vec<f64> = Vec::new();
185 let mut acc = 0.0;
186 for i in 0..data.len() {
187 if flags[i] && i > 0 {
188 sums.push(acc);
189 acc = 0.0;
190 }
191 acc += data[i];
192 }
193 sums.push(acc);
194 sums
195}
196
197pub fn filter_compact<T, F>(data: &[T], predicate: F) -> Vec<T>
206where
207 T: Clone + Send + Sync,
208 F: Fn(&T) -> bool + Sync,
209{
210 data.par_iter().filter(|x| predicate(x)).cloned().collect()
211}
212
213pub fn partition_stable<T, F>(data: &[T], predicate: F) -> (Vec<T>, Vec<T>)
215where
216 T: Clone,
217 F: Fn(&T) -> bool,
218{
219 let mut pass = Vec::new();
220 let mut fail = Vec::new();
221 for x in data {
222 if predicate(x) {
223 pass.push(x.clone());
224 } else {
225 fail.push(x.clone());
226 }
227 }
228 (pass, fail)
229}
230
231pub fn scatter(dst: &mut [f64], src: &[f64], indices: &[usize]) {
239 assert_eq!(
240 src.len(),
241 indices.len(),
242 "src and indices must have equal length"
243 );
244 for (&v, &idx) in src.iter().zip(indices.iter()) {
245 dst[idx] = v;
246 }
247}
248
249pub fn gather(src: &[f64], indices: &[usize]) -> Vec<f64> {
251 indices.iter().map(|&i| src[i]).collect()
252}
253
254pub fn atomic_scatter_add(dst: &mut [f64], src: &[f64], indices: &[usize]) {
258 assert_eq!(src.len(), indices.len());
259 for (&v, &idx) in src.iter().zip(indices.iter()) {
260 dst[idx] += v;
261 }
262}
263
264pub const WARP_SIZE: usize = 32;
270
271pub fn warp_broadcast(lanes: &[f64], leader: usize) -> Vec<f64> {
273 assert!(leader < lanes.len(), "leader lane out of range");
274 vec![lanes[leader]; lanes.len()]
275}
276
277pub fn warp_reduce_sum(lanes: &[f64]) -> Vec<f64> {
279 let total: f64 = lanes.iter().copied().sum();
280 vec![total; lanes.len()]
281}
282
283pub fn warp_exclusive_scan(lanes: &[f64]) -> Vec<f64> {
285 let mut result = vec![0.0; lanes.len()];
286 let mut acc = 0.0;
287 for (i, &v) in lanes.iter().enumerate() {
288 result[i] = acc;
289 acc += v;
290 }
291 result
292}
293
294pub fn warp_vote_any<F: Fn(f64) -> bool>(lanes: &[f64], pred: F) -> bool {
296 lanes.iter().any(|&v| pred(v))
297}
298
299pub fn warp_vote_all<F: Fn(f64) -> bool>(lanes: &[f64], pred: F) -> bool {
301 lanes.iter().all(|&v| pred(v))
302}
303
304pub fn estimate_occupancy(
321 wg_size: usize,
322 regs_per_thread: usize,
323 shared_mem_bytes: usize,
324 max_wgs_per_sm: usize,
325 max_threads_per_sm: usize,
326 max_regs_per_sm: usize,
327 max_smem_per_sm: usize,
328) -> f64 {
329 if wg_size == 0 {
330 return 0.0;
331 }
332 let by_threads = max_threads_per_sm / wg_size;
334 let by_regs = if regs_per_thread == 0 {
335 max_wgs_per_sm
336 } else {
337 max_regs_per_sm / (regs_per_thread * wg_size)
338 };
339 let by_smem = max_smem_per_sm
340 .checked_div(shared_mem_bytes)
341 .unwrap_or(max_wgs_per_sm);
342 let actual_wgs = by_threads.min(by_regs).min(by_smem).min(max_wgs_per_sm);
343 let active_threads = actual_wgs * wg_size;
344 (active_threads as f64 / max_threads_per_sm as f64).min(1.0)
345}
346
347#[derive(Debug, Clone)]
353pub struct GridReduceStats {
354 pub count: usize,
356 pub sum: f64,
358 pub mean: f64,
360 pub variance: f64,
362 pub min: f64,
364 pub max: f64,
366}
367
368impl GridReduceStats {
369 pub fn compute(data: &[f64]) -> Self {
371 let count = data.len();
372 if count == 0 {
373 return Self {
374 count: 0,
375 sum: 0.0,
376 mean: 0.0,
377 variance: 0.0,
378 min: 0.0,
379 max: 0.0,
380 };
381 }
382 let sum: f64 = data.par_iter().copied().sum();
383 let mean = sum / count as f64;
384 let variance: f64 = data
385 .par_iter()
386 .map(|&v| (v - mean) * (v - mean))
387 .sum::<f64>()
388 / count as f64;
389 let min = data.par_iter().copied().reduce(|| f64::INFINITY, f64::min);
390 let max = data
391 .par_iter()
392 .copied()
393 .reduce(|| f64::NEG_INFINITY, f64::max);
394 Self {
395 count,
396 sum,
397 mean,
398 variance,
399 min,
400 max,
401 }
402 }
403
404 pub fn std_dev(&self) -> f64 {
406 self.variance.sqrt()
407 }
408}
409
410#[derive(Debug, Clone)]
419pub struct Histogram {
420 pub bins: Vec<u64>,
422 pub lo: f64,
424 pub hi: f64,
426}
427
428impl Histogram {
429 pub fn compute(data: &[f64], lo: f64, hi: f64, n_bins: usize) -> Self {
433 assert!(n_bins > 0, "n_bins must be > 0");
434 assert!(lo < hi, "lo must be < hi");
435 let width = hi - lo;
436 let mut bins = vec![0u64; n_bins];
437 for &v in data {
438 let idx = ((v - lo) / width * n_bins as f64) as isize;
439 let idx = idx.max(0).min(n_bins as isize - 1) as usize;
440 bins[idx] += 1;
441 }
442 Self { bins, lo, hi }
443 }
444
445 pub fn total(&self) -> u64 {
447 self.bins.iter().sum()
448 }
449
450 pub fn bin_centre(&self, i: usize) -> f64 {
452 let bin_width = (self.hi - self.lo) / self.bins.len() as f64;
453 self.lo + (i as f64 + 0.5) * bin_width
454 }
455
456 pub fn mode_bin(&self) -> usize {
458 self.bins
459 .iter()
460 .enumerate()
461 .max_by_key(|&(_, c)| *c)
462 .map(|(i, _)| i)
463 .unwrap_or(0)
464 }
465
466 pub fn approx_mean(&self) -> f64 {
468 let total = self.total();
469 if total == 0 {
470 return 0.0;
471 }
472 let sum: f64 = self
473 .bins
474 .iter()
475 .enumerate()
476 .map(|(i, &c)| self.bin_centre(i) * c as f64)
477 .sum();
478 sum / total as f64
479 }
480}
481
482pub fn norm_l1(data: &[f64]) -> f64 {
488 data.par_iter().map(|&v| v.abs()).sum()
489}
490
491pub fn norm_l2(data: &[f64]) -> f64 {
493 let sq: f64 = data.par_iter().map(|&v| v * v).sum();
494 sq.sqrt()
495}
496
497pub fn norm_linf(data: &[f64]) -> f64 {
499 data.par_iter()
500 .map(|&v| v.abs())
501 .reduce(|| 0.0_f64, f64::max)
502}
503
504pub fn dist_sq_l2(a: &[f64], b: &[f64]) -> f64 {
506 assert_eq!(a.len(), b.len());
507 a.par_iter()
508 .zip(b.par_iter())
509 .map(|(&ai, &bi)| (ai - bi) * (ai - bi))
510 .sum()
511}
512
513pub fn dist_l2(a: &[f64], b: &[f64]) -> f64 {
515 dist_sq_l2(a, b).sqrt()
516}
517
518pub fn covariance_matrix(data: &[f64], n: usize, d: usize) -> Vec<f64> {
527 assert_eq!(data.len(), n * d, "data must have n*d elements");
528 let mut mean = vec![0.0f64; d];
530 for row in 0..n {
531 for col in 0..d {
532 mean[col] += data[row * d + col];
533 }
534 }
535 for m in &mut mean {
536 *m /= n as f64;
537 }
538
539 let mut cov = vec![0.0f64; d * d];
541 for row in 0..n {
542 for i in 0..d {
543 for j in 0..d {
544 let xi = data[row * d + i] - mean[i];
545 let xj = data[row * d + j] - mean[j];
546 cov[i * d + j] += xi * xj;
547 }
548 }
549 }
550 for c in &mut cov {
551 *c /= n as f64;
552 }
553 cov
554}
555
556pub fn matrix_diagonal(mat: &[f64], d: usize) -> Vec<f64> {
558 (0..d).map(|i| mat[i * d + i]).collect()
559}
560
561pub fn matvec(a: &[f64], m: usize, n: usize, x: &[f64]) -> Vec<f64> {
568 assert_eq!(a.len(), m * n);
569 assert_eq!(x.len(), n);
570 (0..m)
571 .map(|i| {
572 a[i * n..(i + 1) * n]
573 .iter()
574 .zip(x.iter())
575 .map(|(&ai, &xi)| ai * xi)
576 .sum()
577 })
578 .collect()
579}
580
581pub fn matmul(a: &[f64], m: usize, k: usize, b: &[f64], n: usize) -> Vec<f64> {
584 assert_eq!(a.len(), m * k);
585 assert_eq!(b.len(), k * n);
586 let mut c = vec![0.0f64; m * n];
587 for i in 0..m {
588 for p in 0..k {
589 let a_ip = a[i * k + p];
590 for j in 0..n {
591 c[i * n + j] += a_ip * b[p * n + j];
592 }
593 }
594 }
595 c
596}
597
598#[derive(Debug, Clone, Default)]
605pub struct WelfordStats {
606 pub count: u64,
608 pub mean: f64,
610 m2: f64,
612}
613
614impl WelfordStats {
615 pub fn update(&mut self, x: f64) {
617 self.count += 1;
618 let delta = x - self.mean;
619 self.mean += delta / self.count as f64;
620 let delta2 = x - self.mean;
621 self.m2 += delta * delta2;
622 }
623
624 pub fn variance(&self) -> f64 {
626 if self.count < 2 {
627 return 0.0;
628 }
629 self.m2 / self.count as f64
630 }
631
632 pub fn sample_variance(&self) -> f64 {
634 if self.count < 2 {
635 return 0.0;
636 }
637 self.m2 / (self.count - 1) as f64
638 }
639
640 pub fn std_dev(&self) -> f64 {
642 self.variance().sqrt()
643 }
644}
645
646pub fn parallel_histogram(
655 data: &[f64],
656 lo: f64,
657 hi: f64,
658 n_bins: usize,
659 n_workers: usize,
660) -> Vec<u64> {
661 assert!(n_bins > 0);
662 assert!(lo < hi);
663 let chunk_size = data.len().div_ceil(n_workers.max(1));
664 if chunk_size == 0 {
665 return vec![0u64; n_bins];
666 }
667 let partial: Vec<Vec<u64>> = data
668 .par_chunks(chunk_size)
669 .map(|chunk| {
670 let width = hi - lo;
671 let mut bins = vec![0u64; n_bins];
672 for &v in chunk {
673 let idx = ((v - lo) / width * n_bins as f64) as isize;
674 let idx = idx.max(0).min(n_bins as isize - 1) as usize;
675 bins[idx] += 1;
676 }
677 bins
678 })
679 .collect();
680
681 let mut merged = vec![0u64; n_bins];
683 for part in &partial {
684 for (m, &p) in merged.iter_mut().zip(part.iter()) {
685 *m += p;
686 }
687 }
688 merged
689}
690
691pub fn exclusive_scan_u64(data: &[u64]) -> Vec<u64> {
697 let mut result = Vec::with_capacity(data.len());
698 let mut acc = 0u64;
699 for &v in data {
700 result.push(acc);
701 acc = acc.saturating_add(v);
702 }
703 result
704}
705
706pub fn inclusive_scan_u64(data: &[u64]) -> Vec<u64> {
708 let mut result = Vec::with_capacity(data.len());
709 let mut acc = 0u64;
710 for &v in data {
711 acc = acc.saturating_add(v);
712 result.push(acc);
713 }
714 result
715}
716
717pub fn convolve1d(signal: &[f64], kernel: &[f64]) -> Vec<f64> {
726 if signal.is_empty() || kernel.is_empty() {
727 return vec![];
728 }
729 let out_len = signal.len() + kernel.len() - 1;
730 let mut out = vec![0.0f64; out_len];
731 for (i, &s) in signal.iter().enumerate() {
732 for (j, &k) in kernel.iter().enumerate() {
733 out[i + j] += s * k;
734 }
735 }
736 out
737}
738
739pub fn correlate1d_valid(signal: &[f64], pattern: &[f64]) -> Vec<f64> {
742 if pattern.len() > signal.len() {
743 return vec![];
744 }
745 let out_len = signal.len() - pattern.len() + 1;
746 (0..out_len)
747 .map(|i| {
748 signal[i..i + pattern.len()]
749 .iter()
750 .zip(pattern.iter())
751 .map(|(&s, &p)| s * p)
752 .sum()
753 })
754 .collect()
755}
756
757#[cfg(test)]
762mod grid_reduce_tests {
763 use super::*;
764 use crate::grid_reduce::Histogram;
765
766 use crate::grid_reduce::Tile;
767 use crate::grid_reduce::TiledReducer;
768
769 use crate::grid_reduce::WelfordStats;
770
771 use crate::grid_reduce::exclusive_scan_u64;
772
773 use crate::grid_reduce::inclusive_scan_u64;
774
775 use crate::grid_reduce::segmented_reduce_sum;
776
777 #[test]
778 fn test_tile_reduce_sum() {
779 let t = Tile::from_slice(&[1.0, 2.0, 3.0, 4.0]);
780 assert!((t.reduce_sum() - 10.0).abs() < 1e-12);
781 }
782
783 #[test]
784 fn test_tile_exclusive_scan() {
785 let mut t = Tile::from_slice(&[1.0, 2.0, 3.0, 4.0]);
786 t.exclusive_scan_inplace();
787 assert_eq!(t.data, vec![0.0, 1.0, 3.0, 6.0]);
788 }
789
790 #[test]
791 fn test_tile_inclusive_scan() {
792 let mut t = Tile::from_slice(&[1.0, 2.0, 3.0]);
793 t.inclusive_scan_inplace();
794 assert_eq!(t.data, vec![1.0, 3.0, 6.0]);
795 }
796
797 #[test]
798 fn test_tiled_reducer_sum() {
799 let data: Vec<f64> = (1..=100).map(|i| i as f64).collect();
800 let r = TiledReducer::new(16);
801 let s = r.sum(&data);
802 assert!((s - 5050.0).abs() < 1e-8, "sum 1..100 = 5050, got {s}");
803 }
804
805 #[test]
806 fn test_tiled_reducer_dot_product() {
807 let a = vec![1.0, 2.0, 3.0];
808 let b = vec![4.0, 5.0, 6.0];
809 let r = TiledReducer::new(8);
810 let d = r.dot(&a, &b);
811 assert!((d - 32.0).abs() < 1e-12, "dot([1,2,3],[4,5,6]) = 32");
812 }
813
814 #[test]
815 fn test_segmented_exclusive_scan() {
816 let data = [1.0, 2.0, 3.0, 1.0, 2.0];
817 let flags = [true, false, false, true, false];
818 let out = segmented_exclusive_scan(&data, &flags);
819 assert_eq!(out, vec![0.0, 1.0, 3.0, 0.0, 1.0]);
820 }
821
822 #[test]
823 fn test_segmented_reduce_sum() {
824 let data = [1.0, 2.0, 3.0, 10.0, 20.0];
825 let flags = [true, false, false, true, false];
826 let sums = segmented_reduce_sum(&data, &flags);
827 assert_eq!(sums.len(), 2);
828 assert!((sums[0] - 6.0).abs() < 1e-12, "first segment sum = 6");
829 assert!((sums[1] - 30.0).abs() < 1e-12, "second segment sum = 30");
830 }
831
832 #[test]
833 fn test_filter_compact() {
834 let data = vec![1.0, -2.0, 3.0, -4.0, 5.0];
835 let pos: Vec<f64> = filter_compact(&data, |&x| x > 0.0);
836 assert_eq!(pos, vec![1.0, 3.0, 5.0]);
837 }
838
839 #[test]
840 fn test_scatter_gather_roundtrip() {
841 let mut dst = vec![0.0; 5];
842 let src = vec![10.0, 20.0, 30.0];
843 let indices = vec![4, 1, 2];
844 scatter(&mut dst, &src, &indices);
845 assert!((dst[4] - 10.0).abs() < 1e-12);
846 assert!((dst[1] - 20.0).abs() < 1e-12);
847 let gathered = gather(&dst, &[4, 1, 2]);
848 assert_eq!(gathered, vec![10.0, 20.0, 30.0]);
849 }
850
851 #[test]
852 fn test_warp_reduce_sum_all_lanes_equal() {
853 let lanes = vec![1.0, 2.0, 3.0, 4.0];
854 let result = warp_reduce_sum(&lanes);
855 assert!(
856 result.iter().all(|&v| (v - 10.0).abs() < 1e-12),
857 "all lanes should get the total sum"
858 );
859 }
860
861 #[test]
862 fn test_warp_exclusive_scan() {
863 let lanes = vec![1.0, 1.0, 1.0, 1.0];
864 let out = warp_exclusive_scan(&lanes);
865 assert_eq!(out, vec![0.0, 1.0, 2.0, 3.0]);
866 }
867
868 #[test]
869 fn test_occupancy_estimate_full() {
870 let occ = estimate_occupancy(64, 32, 0, 32, 2048, 65536, 49152);
873 assert!((occ - 1.0).abs() < 1e-9, "should be 100% occupancy");
874 }
875
876 #[test]
877 fn test_grid_reduce_stats() {
878 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
879 let stats = GridReduceStats::compute(&data);
880 assert_eq!(stats.count, 5);
881 assert!((stats.sum - 15.0).abs() < 1e-10);
882 assert!((stats.mean - 3.0).abs() < 1e-10);
883 assert!((stats.min - 1.0).abs() < 1e-10);
884 assert!((stats.max - 5.0).abs() < 1e-10);
885 assert!((stats.variance - 2.0).abs() < 1e-10);
887 assert!((stats.std_dev() - 2.0_f64.sqrt()).abs() < 1e-10);
888 }
889
890 #[test]
891 fn test_grid_reduce_stats_empty() {
892 let stats = GridReduceStats::compute(&[]);
893 assert_eq!(stats.count, 0);
894 assert!((stats.sum).abs() < 1e-12);
895 }
896
897 #[test]
900 fn test_histogram_basic() {
901 let data = vec![0.1, 0.5, 0.9, 1.5, 1.9];
902 let h = Histogram::compute(&data, 0.0, 2.0, 2);
903 assert_eq!(h.bins[0], 3);
906 assert_eq!(h.bins[1], 2);
907 assert_eq!(h.total(), 5);
908 }
909
910 #[test]
911 fn test_histogram_mode_bin() {
912 let data = vec![0.1, 0.2, 0.3, 1.5];
913 let h = Histogram::compute(&data, 0.0, 2.0, 2);
914 assert_eq!(h.mode_bin(), 0); }
916
917 #[test]
918 fn test_histogram_bin_centre() {
919 let h = Histogram::compute(&[], 0.0, 4.0, 4);
920 assert!((h.bin_centre(0) - 0.5).abs() < 1e-10);
922 assert!((h.bin_centre(3) - 3.5).abs() < 1e-10);
923 }
924
925 #[test]
926 fn test_histogram_approx_mean() {
927 let data = vec![0.1, 0.2, 0.3, 0.4];
929 let h = Histogram::compute(&data, 0.0, 1.0, 1);
930 assert!((h.approx_mean() - 0.5).abs() < 1e-10);
931 }
932
933 #[test]
936 fn test_norm_l1() {
937 let v = vec![1.0, -2.0, 3.0];
938 assert!((norm_l1(&v) - 6.0).abs() < 1e-12);
939 }
940
941 #[test]
942 fn test_norm_l2() {
943 let v = vec![3.0, 4.0];
944 assert!((norm_l2(&v) - 5.0).abs() < 1e-12);
945 }
946
947 #[test]
948 fn test_norm_linf() {
949 let v = vec![1.0, -5.0, 3.0];
950 assert!((norm_linf(&v) - 5.0).abs() < 1e-12);
951 }
952
953 #[test]
954 fn test_dist_l2() {
955 let a = vec![0.0, 0.0];
956 let b = vec![3.0, 4.0];
957 assert!((dist_l2(&a, &b) - 5.0).abs() < 1e-12);
958 }
959
960 #[test]
963 fn test_covariance_identity_pattern() {
964 let data = vec![0.0, 0.0, 1.0, 1.0, 2.0, 2.0];
967 let cov = covariance_matrix(&data, 3, 2);
968 let expected_var = 2.0 / 3.0;
970 assert!(
971 (cov[0] - expected_var).abs() < 1e-10,
972 "cov[0,0] = {}",
973 cov[0]
974 );
975 assert!(
976 (cov[1] - expected_var).abs() < 1e-10,
977 "cov[0,1] = {}",
978 cov[1]
979 );
980 assert!(
981 (cov[3] - expected_var).abs() < 1e-10,
982 "cov[1,1] = {}",
983 cov[3]
984 );
985 }
986
987 #[test]
988 fn test_matrix_diagonal() {
989 let mat = vec![1.0, 2.0, 3.0, 4.0]; let diag = matrix_diagonal(&mat, 2);
991 assert_eq!(diag, vec![1.0, 4.0]);
992 }
993
994 #[test]
997 fn test_matvec_identity() {
998 let identity = vec![1.0, 0.0, 0.0, 1.0]; let x = vec![3.0, 7.0];
1000 let y = matvec(&identity, 2, 2, &x);
1001 assert_eq!(y, x);
1002 }
1003
1004 #[test]
1005 fn test_matvec_basic() {
1006 let a = vec![1.0, 2.0, 3.0, 4.0];
1008 let x = vec![1.0, 1.0];
1009 let y = matvec(&a, 2, 2, &x);
1010 assert!((y[0] - 3.0).abs() < 1e-12);
1011 assert!((y[1] - 7.0).abs() < 1e-12);
1012 }
1013
1014 #[test]
1015 fn test_matmul_2x2() {
1016 let a = vec![1.0, 2.0, 3.0, 4.0];
1019 let b = vec![5.0, 6.0, 7.0, 8.0];
1020 let c = matmul(&a, 2, 2, &b, 2);
1021 assert!((c[0] - 19.0).abs() < 1e-12);
1022 assert!((c[1] - 22.0).abs() < 1e-12);
1023 assert!((c[2] - 43.0).abs() < 1e-12);
1024 assert!((c[3] - 50.0).abs() < 1e-12);
1025 }
1026
1027 #[test]
1030 fn test_welford_mean_and_variance() {
1031 let mut w = WelfordStats::default();
1032 for &v in &[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
1033 w.update(v);
1034 }
1035 assert!((w.mean - 5.0).abs() < 1e-10, "mean = {}", w.mean);
1036 assert!((w.variance() - 4.0).abs() < 1e-10, "var = {}", w.variance());
1038 }
1039
1040 #[test]
1041 fn test_welford_single_sample() {
1042 let mut w = WelfordStats::default();
1043 w.update(42.0);
1044 assert!((w.mean - 42.0).abs() < 1e-12);
1045 assert!((w.variance()).abs() < 1e-12);
1046 }
1047
1048 #[test]
1051 fn test_parallel_histogram_matches_serial() {
1052 let data: Vec<f64> = (0..200).map(|i| i as f64 / 10.0).collect(); let serial = Histogram::compute(&data, 0.0, 20.0, 10);
1054 let par = parallel_histogram(&data, 0.0, 20.0, 10, 4);
1055 assert_eq!(
1056 serial.bins, par,
1057 "parallel and serial histograms must agree"
1058 );
1059 }
1060
1061 #[test]
1064 fn test_exclusive_scan_u64() {
1065 let data = [1u64, 2, 3, 4];
1066 let out = exclusive_scan_u64(&data);
1067 assert_eq!(out, vec![0, 1, 3, 6]);
1068 }
1069
1070 #[test]
1071 fn test_inclusive_scan_u64() {
1072 let data = [1u64, 2, 3, 4];
1073 let out = inclusive_scan_u64(&data);
1074 assert_eq!(out, vec![1, 3, 6, 10]);
1075 }
1076
1077 #[test]
1080 fn test_convolve1d_basic() {
1081 let sig = vec![1.0, 2.0, 3.0];
1083 let ker = vec![1.0];
1084 let out = convolve1d(&sig, &ker);
1085 assert_eq!(out, sig);
1086 }
1087
1088 #[test]
1089 fn test_convolve1d_box_filter() {
1090 let sig = vec![0.0, 6.0, 0.0]; let ker = vec![1.0, 1.0, 1.0];
1093 let out = convolve1d(&sig, &ker); assert!((out[0]).abs() < 1e-12);
1096 assert!((out[1] - 6.0).abs() < 1e-12);
1097 assert!((out[3] - 6.0).abs() < 1e-12);
1098 assert!((out[4]).abs() < 1e-12);
1099 }
1100
1101 #[test]
1102 fn test_correlate1d_valid() {
1103 let sig = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1104 let pat = vec![1.0, 0.0, -1.0]; let out = correlate1d_valid(&sig, &pat);
1106 assert_eq!(out.len(), 3);
1108 assert!((out[0] - (1.0 - 3.0)).abs() < 1e-12);
1109 assert!((out[1] - (2.0 - 4.0)).abs() < 1e-12);
1110 assert!((out[2] - (3.0 - 5.0)).abs() < 1e-12);
1111 }
1112}
1113
1114pub fn blelloch_exclusive_scan(data: &[f64]) -> Vec<f64> {
1127 if data.is_empty() {
1128 return vec![];
1129 }
1130 let n = data.len();
1132 let mut p = 1usize;
1133 while p < n {
1134 p <<= 1;
1135 }
1136 let mut buf = vec![0.0f64; p];
1137 buf[..n].copy_from_slice(data);
1138
1139 let mut stride = 1usize;
1141 while stride < p {
1142 let step = stride * 2;
1143 let mut i = step - 1;
1144 while i < p {
1145 buf[i] += buf[i - stride];
1146 i += step;
1147 }
1148 stride = step;
1149 }
1150
1151 buf[p - 1] = 0.0;
1153
1154 let mut stride = p / 2;
1156 while stride >= 1 {
1157 let step = stride * 2;
1158 let mut i = step - 1;
1159 while i < p {
1160 let t = buf[i - stride];
1161 buf[i - stride] = buf[i];
1162 buf[i] += t;
1163 i += step;
1164 }
1165 stride /= 2;
1166 }
1167
1168 buf[..n].to_vec()
1169}
1170
1171pub fn blelloch_inclusive_scan(data: &[f64]) -> Vec<f64> {
1173 let excl = blelloch_exclusive_scan(data);
1174 excl.into_iter()
1175 .zip(data.iter())
1176 .map(|(e, &v)| e + v)
1177 .collect()
1178}
1179
1180pub fn blelloch_segmented_exclusive_scan(data: &[f64], flags: &[bool]) -> Vec<f64> {
1189 assert_eq!(data.len(), flags.len());
1190 segmented_exclusive_scan(data, flags)
1193}
1194
1195pub fn parallel_segmented_reduce_sum(data: &[f64], flags: &[bool]) -> Vec<f64> {
1199 assert_eq!(data.len(), flags.len());
1200 let mut starts = vec![0usize];
1202 for (i, &flag) in flags.iter().enumerate().skip(1) {
1203 if flag {
1204 starts.push(i);
1205 }
1206 }
1207 starts.push(data.len());
1208 starts
1209 .windows(2)
1210 .map(|w| data[w[0]..w[1]].iter().sum())
1211 .collect()
1212}
1213
1214pub fn filter_compact_indexed(
1223 data: &[f64],
1224 predicate: impl Fn(f64) -> bool,
1225) -> (Vec<f64>, Vec<usize>) {
1226 let mut vals = Vec::new();
1227 let mut idxs = Vec::new();
1228 for (i, &v) in data.iter().enumerate() {
1229 if predicate(v) {
1230 vals.push(v);
1231 idxs.push(i);
1232 }
1233 }
1234 (vals, idxs)
1235}
1236
1237pub fn filter_compact_counted<T: Clone>(
1239 data: &[T],
1240 predicate: impl Fn(&T) -> bool,
1241) -> (Vec<T>, usize) {
1242 let compacted: Vec<T> = data.iter().filter(|x| predicate(x)).cloned().collect();
1243 let n_removed = data.len() - compacted.len();
1244 (compacted, n_removed)
1245}
1246
1247pub fn radix_sort_pass_u64(data: &[u64], bit_pos: u32, radix: usize) -> Vec<u64> {
1257 assert!(radix.is_power_of_two(), "radix must be a power of two");
1258 let mask = (radix - 1) as u64;
1259 let mut counts = vec![0usize; radix];
1261 for &v in data {
1262 let digit = ((v >> bit_pos) & mask) as usize;
1263 counts[digit] += 1;
1264 }
1265 let offsets = exclusive_scan_u64(&counts.iter().map(|&c| c as u64).collect::<Vec<_>>());
1267 let mut offsets: Vec<usize> = offsets.iter().map(|&o| o as usize).collect();
1268 let mut out = vec![0u64; data.len()];
1270 for &v in data {
1271 let digit = ((v >> bit_pos) & mask) as usize;
1272 out[offsets[digit]] = v;
1273 offsets[digit] += 1;
1274 }
1275 out
1276}
1277
1278pub fn radix_sort_u64(data: &[u64]) -> Vec<u64> {
1280 let mut buf = data.to_vec();
1281 for pass in 0..8u32 {
1282 buf = radix_sort_pass_u64(&buf, pass * 8, 256);
1283 }
1284 buf
1285}
1286
1287pub fn radix_sort_f64(data: &[f64]) -> Vec<f64> {
1292 let mut keys: Vec<u64> = data
1293 .iter()
1294 .map(|&v| {
1295 let bits = v.to_bits();
1296 if bits >> 63 == 0 {
1297 bits | (1u64 << 63) } else {
1299 !bits }
1301 })
1302 .collect();
1303 keys = radix_sort_u64(&keys);
1304 keys.iter()
1305 .map(|&bits| {
1306 let recovered = if bits >> 63 == 1 {
1307 bits ^ (1u64 << 63) } else {
1309 !bits };
1311 f64::from_bits(recovered)
1312 })
1313 .collect()
1314}
1315
1316pub fn tree_reduce_sum(data: &[f64]) -> f64 {
1325 if data.is_empty() {
1326 return 0.0;
1327 }
1328 let mut buf = data.to_vec();
1329 let mut n = buf.len();
1330 while n > 1 {
1331 let half = n / 2;
1332 for i in 0..half {
1333 buf[i] += buf[i + half];
1334 }
1335 if n % 2 == 1 {
1336 buf[half - 1] += buf[n - 1];
1337 }
1338 n = half;
1339 }
1340 buf[0]
1341}
1342
1343pub fn tree_reduce_max(data: &[f64]) -> f64 {
1345 if data.is_empty() {
1346 return f64::NEG_INFINITY;
1347 }
1348 let mut buf = data.to_vec();
1349 let mut n = buf.len();
1350 while n > 1 {
1351 let half = n / 2;
1352 for i in 0..half {
1353 buf[i] = f64::max(buf[i], buf[i + half]);
1354 }
1355 if n % 2 == 1 {
1356 buf[half - 1] = f64::max(buf[half - 1], buf[n - 1]);
1357 }
1358 n = half;
1359 }
1360 buf[0]
1361}
1362
1363pub fn tree_reduce_min(data: &[f64]) -> f64 {
1365 if data.is_empty() {
1366 return f64::INFINITY;
1367 }
1368 let mut buf = data.to_vec();
1369 let mut n = buf.len();
1370 while n > 1 {
1371 let half = n / 2;
1372 for i in 0..half {
1373 buf[i] = f64::min(buf[i], buf[i + half]);
1374 }
1375 if n % 2 == 1 {
1376 buf[half - 1] = f64::min(buf[half - 1], buf[n - 1]);
1377 }
1378 n = half;
1379 }
1380 buf[0]
1381}
1382
1383pub fn reduce_broadcast(data: &[f64]) -> Vec<f64> {
1391 let total: f64 = data.iter().copied().sum();
1392 vec![total; data.len()]
1393}
1394
1395pub fn normalise_by_sum(data: &[f64]) -> Vec<f64> {
1397 let s: f64 = data.iter().copied().sum();
1398 if s.abs() < 1e-30 {
1399 return data.to_vec();
1400 }
1401 data.iter().map(|&v| v / s).collect()
1402}
1403
1404#[derive(Debug, Clone)]
1412pub struct TwoLevelHistogram {
1413 pub bins: Vec<u64>,
1415 pub lo: f64,
1417 pub hi: f64,
1419 pub n_tiles: usize,
1421}
1422
1423impl TwoLevelHistogram {
1424 pub fn compute(data: &[f64], lo: f64, hi: f64, n_bins: usize, tile_size: usize) -> Self {
1426 let n_tiles = data.len().div_ceil(tile_size.max(1));
1427 let bins = parallel_histogram(data, lo, hi, n_bins, n_tiles.max(1));
1428 Self {
1429 bins,
1430 lo,
1431 hi,
1432 n_tiles,
1433 }
1434 }
1435
1436 pub fn total(&self) -> u64 {
1438 self.bins.iter().sum()
1439 }
1440
1441 pub fn approx_median(&self) -> f64 {
1443 let total = self.total();
1444 if total == 0 {
1445 return (self.lo + self.hi) / 2.0;
1446 }
1447 let half = total / 2;
1448 let n = self.bins.len() as f64;
1449 let mut acc = 0u64;
1450 for (i, &c) in self.bins.iter().enumerate() {
1451 acc += c;
1452 if acc >= half {
1453 let bin_width = (self.hi - self.lo) / n;
1454 return self.lo + (i as f64 + 0.5) * bin_width;
1455 }
1456 }
1457 self.hi
1458 }
1459}
1460
1461#[derive(Debug, Clone, Default)]
1467pub struct RunningMinMax {
1468 pub min: f64,
1470 pub max: f64,
1472 pub count: u64,
1474}
1475
1476impl RunningMinMax {
1477 pub fn new() -> Self {
1479 Self {
1480 min: f64::INFINITY,
1481 max: f64::NEG_INFINITY,
1482 count: 0,
1483 }
1484 }
1485
1486 pub fn update(&mut self, v: f64) {
1488 self.min = f64::min(self.min, v);
1489 self.max = f64::max(self.max, v);
1490 self.count += 1;
1491 }
1492
1493 pub fn update_slice(&mut self, data: &[f64]) {
1495 for &v in data {
1496 self.update(v);
1497 }
1498 }
1499
1500 pub fn range(&self) -> f64 {
1502 if self.count == 0 {
1503 return 0.0;
1504 }
1505 self.max - self.min
1506 }
1507}
1508
1509pub fn compact_scatter(src: &[f64], mask: &[bool], dst: &mut Vec<f64>) -> usize {
1518 assert_eq!(src.len(), mask.len());
1519 let before = dst.len();
1520 for (&v, &keep) in src.iter().zip(mask.iter()) {
1521 if keep {
1522 dst.push(v);
1523 }
1524 }
1525 dst.len() - before
1526}
1527
1528pub fn compaction_offsets(mask: &[bool]) -> Vec<usize> {
1533 let mut result = vec![usize::MAX; mask.len()];
1534 let mut counter = 0usize;
1535 for (i, &keep) in mask.iter().enumerate() {
1536 if keep {
1537 result[i] = counter;
1538 counter += 1;
1539 }
1540 }
1541 result
1542}
1543
1544#[cfg(test)]
1549mod extended_tests {
1550 use crate::grid_reduce::Histogram;
1551 use crate::grid_reduce::RunningMinMax;
1552 use crate::grid_reduce::Tile;
1553 use crate::grid_reduce::TiledReducer;
1554 use crate::grid_reduce::TwoLevelHistogram;
1555 use crate::grid_reduce::WelfordStats;
1556 use crate::grid_reduce::blelloch_exclusive_scan;
1557 use crate::grid_reduce::blelloch_inclusive_scan;
1558 use crate::grid_reduce::compact_scatter;
1559 use crate::grid_reduce::compaction_offsets;
1560 use crate::grid_reduce::exclusive_scan_u64;
1561 use crate::grid_reduce::filter_compact_counted;
1562 use crate::grid_reduce::filter_compact_indexed;
1563 use crate::grid_reduce::inclusive_scan_u64;
1564 use crate::grid_reduce::normalise_by_sum;
1565 use crate::grid_reduce::parallel_segmented_reduce_sum;
1566 use crate::grid_reduce::radix_sort_f64;
1567 use crate::grid_reduce::radix_sort_pass_u64;
1568 use crate::grid_reduce::radix_sort_u64;
1569 use crate::grid_reduce::reduce_broadcast;
1570 use crate::grid_reduce::segmented_reduce_sum;
1571 use crate::grid_reduce::tree_reduce_max;
1572 use crate::grid_reduce::tree_reduce_min;
1573 use crate::grid_reduce::tree_reduce_sum;
1574
1575 #[test]
1578 fn blelloch_exclusive_scan_matches_serial() {
1579 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1580 let serial = {
1581 let mut r = Vec::new();
1582 let mut acc = 0.0f64;
1583 for &v in &data {
1584 r.push(acc);
1585 acc += v;
1586 }
1587 r
1588 };
1589 let blelloch = blelloch_exclusive_scan(&data);
1590 for (a, b) in serial.iter().zip(blelloch.iter()) {
1591 assert!((a - b).abs() < 1e-10, "mismatch: serial={a} blelloch={b}");
1592 }
1593 }
1594
1595 #[test]
1596 fn blelloch_exclusive_scan_non_pow2() {
1597 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0]; let result = blelloch_exclusive_scan(&data);
1599 assert_eq!(result.len(), 5);
1600 assert!((result[0] - 0.0).abs() < 1e-10);
1601 assert!((result[1] - 1.0).abs() < 1e-10);
1602 assert!((result[2] - 3.0).abs() < 1e-10);
1603 assert!((result[3] - 6.0).abs() < 1e-10);
1604 assert!((result[4] - 10.0).abs() < 1e-10);
1605 }
1606
1607 #[test]
1608 fn blelloch_inclusive_scan_correct() {
1609 let data = vec![1.0, 2.0, 3.0, 4.0];
1610 let result = blelloch_inclusive_scan(&data);
1611 assert_eq!(result, vec![1.0, 3.0, 6.0, 10.0]);
1612 }
1613
1614 #[test]
1615 fn blelloch_exclusive_scan_single_element() {
1616 let result = blelloch_exclusive_scan(&[42.0]);
1617 assert_eq!(result, vec![0.0]);
1618 }
1619
1620 #[test]
1621 fn blelloch_exclusive_scan_all_zeros() {
1622 let data = vec![0.0; 8];
1623 let result = blelloch_exclusive_scan(&data);
1624 assert!(result.iter().all(|&v| v.abs() < 1e-12));
1625 }
1626
1627 #[test]
1630 fn parallel_segmented_reduce_matches_serial() {
1631 let data = [1.0, 2.0, 3.0, 10.0, 20.0, 30.0];
1632 let flags = [true, false, false, true, false, false];
1633 let par = parallel_segmented_reduce_sum(&data, &flags);
1634 let ser = segmented_reduce_sum(&data, &flags);
1635 assert_eq!(par, ser);
1636 }
1637
1638 #[test]
1639 fn parallel_segmented_reduce_single_segment() {
1640 let data = [1.0, 2.0, 3.0];
1641 let flags = [true, false, false];
1642 let result = parallel_segmented_reduce_sum(&data, &flags);
1643 assert_eq!(result.len(), 1);
1644 assert!((result[0] - 6.0).abs() < 1e-10);
1645 }
1646
1647 #[test]
1650 fn filter_compact_indexed_positive() {
1651 let data = vec![-1.0, 2.0, -3.0, 4.0, 5.0];
1652 let (vals, idxs) = filter_compact_indexed(&data, |v| v > 0.0);
1653 assert_eq!(vals, vec![2.0, 4.0, 5.0]);
1654 assert_eq!(idxs, vec![1, 3, 4]);
1655 }
1656
1657 #[test]
1658 fn filter_compact_indexed_empty_result() {
1659 let data = vec![-1.0, -2.0, -3.0];
1660 let (vals, idxs) = filter_compact_indexed(&data, |v| v > 0.0);
1661 assert!(vals.is_empty());
1662 assert!(idxs.is_empty());
1663 }
1664
1665 #[test]
1666 fn filter_compact_counted_removes_negatives() {
1667 let data = vec![1.0, -2.0, 3.0, -4.0, 5.0];
1668 let (kept, removed) = filter_compact_counted(&data, |v| *v >= 0.0);
1669 assert_eq!(kept, vec![1.0, 3.0, 5.0]);
1670 assert_eq!(removed, 2);
1671 }
1672
1673 #[test]
1676 fn radix_sort_u64_ascending() {
1677 let mut data = vec![5u64, 3, 8, 1, 9, 2, 7, 4, 6, 0];
1678 let sorted = radix_sort_u64(&data);
1679 data.sort_unstable();
1680 assert_eq!(sorted, data);
1681 }
1682
1683 #[test]
1684 fn radix_sort_u64_empty() {
1685 let sorted = radix_sort_u64(&[]);
1686 assert!(sorted.is_empty());
1687 }
1688
1689 #[test]
1690 fn radix_sort_u64_already_sorted() {
1691 let data = vec![1u64, 2, 3, 4, 5];
1692 assert_eq!(radix_sort_u64(&data), data);
1693 }
1694
1695 #[test]
1696 fn radix_sort_u64_reverse() {
1697 let data = vec![5u64, 4, 3, 2, 1];
1698 let sorted = radix_sort_u64(&data);
1699 assert_eq!(sorted, vec![1u64, 2, 3, 4, 5]);
1700 }
1701
1702 #[test]
1703 fn radix_sort_f64_positive_values() {
1704 let data = vec![3.125, 1.41, 2.71, 0.57, 1.73];
1705 let sorted = radix_sort_f64(&data);
1706 let mut expected = data.clone();
1707 expected.sort_by(|a, b| a.partial_cmp(b).unwrap());
1708 for (a, b) in sorted.iter().zip(expected.iter()) {
1709 assert!((a - b).abs() < 1e-12, "a={a} b={b}");
1710 }
1711 }
1712
1713 #[test]
1714 fn radix_sort_pass_u64_single_pass() {
1715 let data = vec![0x03u64, 0x01, 0x04, 0x01, 0x05];
1717 let sorted = radix_sort_pass_u64(&data, 0, 256);
1718 assert_eq!(sorted.len(), data.len());
1719 for w in sorted.windows(2) {
1721 assert!(w[0] & 0xFF <= w[1] & 0xFF, "not sorted by low byte");
1722 }
1723 }
1724
1725 #[test]
1728 fn tree_reduce_sum_correct() {
1729 let data: Vec<f64> = (1..=16).map(|i| i as f64).collect();
1730 let s = tree_reduce_sum(&data);
1731 assert!((s - 136.0).abs() < 1e-10, "sum = {s}");
1732 }
1733
1734 #[test]
1735 fn tree_reduce_sum_odd_length() {
1736 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1737 let s = tree_reduce_sum(&data);
1738 assert!((s - 15.0).abs() < 1e-10, "sum = {s}");
1739 }
1740
1741 #[test]
1742 fn tree_reduce_max_correct() {
1743 let data = vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
1744 assert!((tree_reduce_max(&data) - 9.0).abs() < 1e-12);
1745 }
1746
1747 #[test]
1748 fn tree_reduce_min_correct() {
1749 let data = vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
1750 assert!((tree_reduce_min(&data) - 1.0).abs() < 1e-12);
1751 }
1752
1753 #[test]
1754 fn tree_reduce_empty() {
1755 assert!((tree_reduce_sum(&[])).abs() < 1e-12);
1756 assert!(tree_reduce_max(&[]) == f64::NEG_INFINITY);
1757 assert!(tree_reduce_min(&[]) == f64::INFINITY);
1758 }
1759
1760 #[test]
1761 fn tree_reduce_single() {
1762 assert!((tree_reduce_sum(&[42.0]) - 42.0).abs() < 1e-12);
1763 assert!((tree_reduce_max(&[42.0]) - 42.0).abs() < 1e-12);
1764 assert!((tree_reduce_min(&[42.0]) - 42.0).abs() < 1e-12);
1765 }
1766
1767 #[test]
1768 fn tree_reduce_matches_tiled_reducer() {
1769 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1770 let tr = TiledReducer::new(16);
1771 let tiled_sum = tr.sum(&data);
1772 let tree_sum = tree_reduce_sum(&data);
1773 assert!(
1774 (tiled_sum - tree_sum).abs() < 1e-8,
1775 "tiled={tiled_sum} tree={tree_sum}"
1776 );
1777 }
1778
1779 #[test]
1782 fn reduce_broadcast_all_equal() {
1783 let data = vec![1.0, 2.0, 3.0];
1784 let result = reduce_broadcast(&data);
1785 assert!(
1786 result.iter().all(|&v| (v - 6.0).abs() < 1e-12),
1787 "all should equal 6"
1788 );
1789 }
1790
1791 #[test]
1792 fn normalise_by_sum_sums_to_one() {
1793 let data = vec![1.0, 2.0, 3.0, 4.0];
1794 let normed = normalise_by_sum(&data);
1795 let s: f64 = normed.iter().sum();
1796 assert!((s - 1.0).abs() < 1e-10, "sum = {s}");
1797 }
1798
1799 #[test]
1800 fn normalise_by_sum_zero_input_unchanged() {
1801 let data = vec![0.0, 0.0, 0.0];
1802 let result = normalise_by_sum(&data);
1803 assert_eq!(result, data);
1804 }
1805
1806 #[test]
1809 fn two_level_histogram_total_correct() {
1810 let data: Vec<f64> = (0..100).map(|i| i as f64 / 10.0).collect();
1811 let h = TwoLevelHistogram::compute(&data, 0.0, 10.0, 10, 16);
1812 assert_eq!(h.total(), 100);
1813 }
1814
1815 #[test]
1816 fn two_level_histogram_approx_median() {
1817 let data: Vec<f64> = (0..1000).map(|i| i as f64 / 100.0).collect();
1819 let h = TwoLevelHistogram::compute(&data, 0.0, 10.0, 100, 64);
1820 let med = h.approx_median();
1821 assert!((med - 5.0).abs() < 0.2, "approx median = {med}");
1822 }
1823
1824 #[test]
1825 fn two_level_histogram_bins_count_matches() {
1826 let data = vec![0.5, 1.5, 2.5, 3.5];
1827 let h = TwoLevelHistogram::compute(&data, 0.0, 4.0, 4, 2);
1828 assert_eq!(h.total(), 4);
1829 for &c in &h.bins {
1830 assert_eq!(c, 1, "each bin should have 1 element");
1831 }
1832 }
1833
1834 #[test]
1837 fn running_min_max_basic() {
1838 let mut t = RunningMinMax::new();
1839 t.update_slice(&[3.0, 1.0, 4.0, 1.0, 5.0]);
1840 assert!((t.min - 1.0).abs() < 1e-12);
1841 assert!((t.max - 5.0).abs() < 1e-12);
1842 assert_eq!(t.count, 5);
1843 assert!((t.range() - 4.0).abs() < 1e-12);
1844 }
1845
1846 #[test]
1847 fn running_min_max_single() {
1848 let mut t = RunningMinMax::new();
1849 t.update(42.0);
1850 assert!((t.min - 42.0).abs() < 1e-12);
1851 assert!((t.max - 42.0).abs() < 1e-12);
1852 assert!((t.range()).abs() < 1e-12);
1853 }
1854
1855 #[test]
1856 fn running_min_max_empty_range() {
1857 let t = RunningMinMax::new();
1858 assert!((t.range()).abs() < 1e-12);
1859 }
1860
1861 #[test]
1864 fn compact_scatter_basic() {
1865 let src = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1866 let mask = vec![true, false, true, false, true];
1867 let mut dst = Vec::new();
1868 let n = compact_scatter(&src, &mask, &mut dst);
1869 assert_eq!(n, 3);
1870 assert_eq!(dst, vec![1.0, 3.0, 5.0]);
1871 }
1872
1873 #[test]
1874 fn compact_scatter_appends_to_existing() {
1875 let src = vec![10.0, 20.0];
1876 let mask = vec![true, true];
1877 let mut dst = vec![0.0, 0.0];
1878 compact_scatter(&src, &mask, &mut dst);
1879 assert_eq!(dst, vec![0.0, 0.0, 10.0, 20.0]);
1880 }
1881
1882 #[test]
1883 fn compaction_offsets_correct() {
1884 let mask = vec![true, false, true, false, true];
1885 let offsets = compaction_offsets(&mask);
1886 assert_eq!(offsets[0], 0);
1887 assert_eq!(offsets[1], usize::MAX);
1888 assert_eq!(offsets[2], 1);
1889 assert_eq!(offsets[3], usize::MAX);
1890 assert_eq!(offsets[4], 2);
1891 }
1892
1893 #[test]
1894 fn compaction_offsets_all_false() {
1895 let mask = vec![false; 5];
1896 let offsets = compaction_offsets(&mask);
1897 assert!(offsets.iter().all(|&o| o == usize::MAX));
1898 }
1899
1900 #[test]
1903 fn histogram_uniform_distribution() {
1904 let data: Vec<f64> = (0..10).map(|i| i as f64 + 0.5).collect();
1905 let h = Histogram::compute(&data, 0.0, 10.0, 10);
1906 for &c in &h.bins {
1907 assert_eq!(c, 1, "each bin should have exactly 1 element");
1908 }
1909 }
1910
1911 #[test]
1912 fn histogram_clamped_out_of_range() {
1913 let data = vec![-5.0, 5.0, 15.0]; let h = Histogram::compute(&data, 0.0, 10.0, 2);
1915 assert_eq!(
1916 h.total(),
1917 3,
1918 "out-of-range values should be clamped into boundary bins"
1919 );
1920 }
1921
1922 #[test]
1925 fn welford_sample_variance_two_samples() {
1926 let mut w = WelfordStats::default();
1927 w.update(2.0);
1928 w.update(4.0);
1929 let sv = w.sample_variance();
1931 assert!((sv - 2.0).abs() < 1e-10, "sample_var = {sv}");
1932 }
1933
1934 #[test]
1935 fn welford_std_dev_known_dataset() {
1936 let mut w = WelfordStats::default();
1937 for &v in &[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
1938 w.update(v);
1939 }
1940 assert!(
1941 (w.std_dev() - 2.0).abs() < 1e-10,
1942 "std_dev = {}",
1943 w.std_dev()
1944 );
1945 }
1946
1947 #[test]
1950 fn exclusive_scan_u64_empty() {
1951 let r = exclusive_scan_u64(&[]);
1952 assert!(r.is_empty());
1953 }
1954
1955 #[test]
1956 fn inclusive_scan_u64_single() {
1957 let r = inclusive_scan_u64(&[7u64]);
1958 assert_eq!(r, vec![7]);
1959 }
1960
1961 #[test]
1964 fn tile_reduce_max_and_min() {
1965 let t = Tile::from_slice(&[3.0, 1.0, 4.0, 1.0, 5.0]);
1966 assert!((t.reduce_max() - 5.0).abs() < 1e-12);
1967 assert!((t.reduce_min() - 1.0).abs() < 1e-12);
1968 }
1969
1970 #[test]
1971 fn tiled_reducer_tile_sums_length() {
1972 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1973 let r = TiledReducer::new(16);
1974 let ts = r.tile_sums(&data);
1975 assert_eq!(ts.len(), 7); }
1977
1978 #[test]
1979 fn tiled_reducer_max_and_min() {
1980 let data = vec![-5.0, 3.0, 8.0, -1.0, 2.0];
1981 let r = TiledReducer::new(4);
1982 assert!((r.max(&data) - 8.0).abs() < 1e-12);
1983 assert!((r.min(&data) - (-5.0)).abs() < 1e-12);
1984 }
1985}