Skip to main content

oxiphysics_gpu/
grid_reduce.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! GPU-style grid-based reduction and aggregation kernels.
5//!
6//! Provides tiled parallel reductions, segmented scans, work-group min/max/sum
7//! operations, warp-level primitives (simulated on CPU), stream compaction
8//! (filter_compact), sparse-to-dense scatter/gather, and occupancy estimation
9//! helpers.  All algorithms are CPU-side mocks that mimic GPU execution
10//! semantics using Rayon.
11
12use rayon::prelude::*;
13
14// ---------------------------------------------------------------------------
15// 1. Tile — a fixed-size work-group window
16// ---------------------------------------------------------------------------
17
18/// A single work-group tile holding up to `CAPACITY` f64 values.
19#[derive(Debug, Clone)]
20pub struct Tile {
21    /// Elements in this tile.
22    pub data: Vec<f64>,
23}
24
25impl Tile {
26    /// Create a tile from a slice.
27    pub fn from_slice(s: &[f64]) -> Self {
28        Self { data: s.to_vec() }
29    }
30
31    /// Reduce: sum of all elements.
32    pub fn reduce_sum(&self) -> f64 {
33        self.data.iter().copied().sum()
34    }
35
36    /// Reduce: maximum element.
37    pub fn reduce_max(&self) -> f64 {
38        self.data.iter().copied().fold(f64::NEG_INFINITY, f64::max)
39    }
40
41    /// Reduce: minimum element.
42    pub fn reduce_min(&self) -> f64 {
43        self.data.iter().copied().fold(f64::INFINITY, f64::min)
44    }
45
46    /// Exclusive prefix scan within the tile (in-place).
47    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    /// Inclusive prefix scan within the tile (in-place).
57    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// ---------------------------------------------------------------------------
67// 2. TiledReducer — parallel multi-tile reduction
68// ---------------------------------------------------------------------------
69
70/// Parallel tiled reducer.
71///
72/// Splits the input into tiles of `tile_size`, performs a local reduction
73/// on each tile in parallel, then reduces the per-tile results serially.
74#[derive(Debug, Clone)]
75pub struct TiledReducer {
76    /// Number of elements per tile (analogous to GPU work-group size).
77    pub tile_size: usize,
78}
79
80impl TiledReducer {
81    /// Create a new tiled reducer.
82    pub fn new(tile_size: usize) -> Self {
83        assert!(tile_size > 0, "tile_size must be > 0");
84        Self { tile_size }
85    }
86
87    /// Compute the global sum using two-level tiled reduction.
88    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    /// Compute the global maximum.
100    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    /// Compute the global minimum.
112    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    /// Compute the global dot product of two equal-length slices.
124    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    /// Compute per-tile sums (exposes intermediate tile results).
133    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
140// ---------------------------------------------------------------------------
141// 3. SegmentedScan — prefix scan within variable-length segments
142// ---------------------------------------------------------------------------
143
144/// Segmented exclusive prefix scan.
145///
146/// Each segment boundary is marked by a `true` entry in `flags`.  The scan
147/// resets to 0 at the start of each segment.
148pub 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; // new segment
159        }
160        result[i] = acc;
161        acc += data[i];
162    }
163    result
164}
165
166/// Segmented inclusive prefix scan.
167pub 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
181/// Segmented reduce: sum within each segment, returns one value per segment.
182pub 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
197// ---------------------------------------------------------------------------
198// 4. Stream Compaction (filter_compact)
199// ---------------------------------------------------------------------------
200
201/// Stream compaction: collect elements satisfying `predicate` into a new vec.
202///
203/// Mimics GPU stream compaction (prefix-sum + scatter).
204/// The output preserves the relative order of passing elements.
205pub 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
213/// Partition `data` into two groups: (passing, failing) — stable order.
214pub 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
231// ---------------------------------------------------------------------------
232// 5. Scatter / Gather
233// ---------------------------------------------------------------------------
234
235/// Scatter: write `src[i]` to `dst[indices[i\]]`.
236///
237/// Panics if any index is out of bounds.
238pub 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
249/// Gather: collect `src[indices[i\]]` into a new vec.
250pub fn gather(src: &[f64], indices: &[usize]) -> Vec<f64> {
251    indices.iter().map(|&i| src[i]).collect()
252}
253
254/// Atomic-add scatter (simulated serially): `dst[idx] += value`.
255///
256/// In a real GPU kernel this would use `atomicAdd`.
257pub 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
264// ---------------------------------------------------------------------------
265// 6. Warp-level primitives (simulated on CPU as fixed-width groups)
266// ---------------------------------------------------------------------------
267
268/// Simulated warp size: number of lanes in one warp.
269pub const WARP_SIZE: usize = 32;
270
271/// Simulate a warp-level broadcast: every lane gets `lane_val[leader]`.
272pub 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
277/// Simulate a warp-level reduce-sum: all lanes get the total sum.
278pub fn warp_reduce_sum(lanes: &[f64]) -> Vec<f64> {
279    let total: f64 = lanes.iter().copied().sum();
280    vec![total; lanes.len()]
281}
282
283/// Simulate a warp-level exclusive scan.
284pub 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
294/// Simulate warp vote: `any` — returns true if any lane passes `pred`.
295pub fn warp_vote_any<F: Fn(f64) -> bool>(lanes: &[f64], pred: F) -> bool {
296    lanes.iter().any(|&v| pred(v))
297}
298
299/// Simulate warp vote: `all` — returns true if all lanes pass `pred`.
300pub fn warp_vote_all<F: Fn(f64) -> bool>(lanes: &[f64], pred: F) -> bool {
301    lanes.iter().all(|&v| pred(v))
302}
303
304// ---------------------------------------------------------------------------
305// 7. Occupancy estimation helper
306// ---------------------------------------------------------------------------
307
308/// Compute the theoretical SM occupancy given resource usage.
309///
310/// Returns a value in `[0.0, 1.0]` where 1.0 = 100% occupancy.
311///
312/// # Parameters
313/// * `wg_size`         - threads per work-group.
314/// * `regs_per_thread` - registers used per thread.
315/// * `shared_mem_bytes`- shared memory used per work-group.
316/// * `max_wgs_per_sm`  - hardware limit (work-groups per SM).
317/// * `max_threads_per_sm` - hardware limit (threads per SM).
318/// * `max_regs_per_sm` - hardware limit (total registers per SM).
319/// * `max_smem_per_sm` - hardware limit (shared memory bytes per SM).
320pub 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    // Maximum work-groups limited by each resource.
333    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// ---------------------------------------------------------------------------
348// 8. GridReduceStats — aggregate statistics over a 3-D grid
349// ---------------------------------------------------------------------------
350
351/// Aggregate statistics computed over a 3-D grid of f64 values.
352#[derive(Debug, Clone)]
353pub struct GridReduceStats {
354    /// Total number of elements.
355    pub count: usize,
356    /// Sum of all elements.
357    pub sum: f64,
358    /// Mean value.
359    pub mean: f64,
360    /// Variance (population).
361    pub variance: f64,
362    /// Minimum value.
363    pub min: f64,
364    /// Maximum value.
365    pub max: f64,
366}
367
368impl GridReduceStats {
369    /// Compute statistics from a flat slice using parallel reductions.
370    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    /// Standard deviation.
405    pub fn std_dev(&self) -> f64 {
406        self.variance.sqrt()
407    }
408}
409
410// ---------------------------------------------------------------------------
411// 9. Histogram kernel (parallel, fixed-bin-count)
412// ---------------------------------------------------------------------------
413
414/// Fixed-bin histogram over a f64 slice.
415///
416/// Values outside `[lo, hi)` are clamped into the boundary bins.
417/// Mimics a GPU atomic histogram with one thread per element.
418#[derive(Debug, Clone)]
419pub struct Histogram {
420    /// Bin counts.
421    pub bins: Vec<u64>,
422    /// Lower bound of the first bin.
423    pub lo: f64,
424    /// Upper bound of the last bin (exclusive).
425    pub hi: f64,
426}
427
428impl Histogram {
429    /// Compute a histogram with `n_bins` bins over `[lo, hi)`.
430    ///
431    /// Panics if `n_bins == 0` or `lo >= hi`.
432    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    /// Total count of elements in all bins.
446    pub fn total(&self) -> u64 {
447        self.bins.iter().sum()
448    }
449
450    /// Centre value of bin `i`.
451    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    /// Index of the most-populated bin (mode).
457    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    /// Approximate mean computed from bin centres.
467    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
482// ---------------------------------------------------------------------------
483// 10. L1 / L2 / Linf norms
484// ---------------------------------------------------------------------------
485
486/// L1 norm: sum of absolute values.
487pub fn norm_l1(data: &[f64]) -> f64 {
488    data.par_iter().map(|&v| v.abs()).sum()
489}
490
491/// L2 (Euclidean) norm.
492pub fn norm_l2(data: &[f64]) -> f64 {
493    let sq: f64 = data.par_iter().map(|&v| v * v).sum();
494    sq.sqrt()
495}
496
497/// L∞ (Chebyshev) norm: maximum absolute value.
498pub fn norm_linf(data: &[f64]) -> f64 {
499    data.par_iter()
500        .map(|&v| v.abs())
501        .reduce(|| 0.0_f64, f64::max)
502}
503
504/// Squared L2 distance between two equal-length vectors.
505pub 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
513/// L2 distance between two equal-length vectors.
514pub fn dist_l2(a: &[f64], b: &[f64]) -> f64 {
515    dist_sq_l2(a, b).sqrt()
516}
517
518// ---------------------------------------------------------------------------
519// 11. Covariance matrix (parallel, CPU mock)
520// ---------------------------------------------------------------------------
521
522/// Compute the `d×d` (population) covariance matrix for `n` observations of
523/// dimension `d`, stored row-major in `data` (shape `n × d`).
524///
525/// Returns a flat `d*d` vector, row-major.
526pub 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    // Mean of each dimension
529    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    // Covariance: C[i][j] = E[(X_i - mean_i)(X_j - mean_j)]
540    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
556/// Extract the diagonal of a `d×d` matrix stored as a flat `d*d` slice.
557pub fn matrix_diagonal(mat: &[f64], d: usize) -> Vec<f64> {
558    (0..d).map(|i| mat[i * d + i]).collect()
559}
560
561// ---------------------------------------------------------------------------
562// 12. Dense matrix-vector multiply (CPU mock GPU GEMV)
563// ---------------------------------------------------------------------------
564
565/// Compute `y = A * x` where `A` is `m × n` (row-major), `x` has `n`
566/// elements, result `y` has `m` elements.
567pub 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
581/// Compute `C = A * B` where `A` is `m × k` and `B` is `k × n` (all row-major).
582/// Returns a flat `m*n` vector.
583pub 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// ---------------------------------------------------------------------------
599// 13. Running statistics accumulator (Welford online algorithm)
600// ---------------------------------------------------------------------------
601
602/// Welford online statistics: accumulates count, mean, and variance in O(1)
603/// per sample.  Suitable for streaming GPU readback values.
604#[derive(Debug, Clone, Default)]
605pub struct WelfordStats {
606    /// Number of samples observed.
607    pub count: u64,
608    /// Current mean.
609    pub mean: f64,
610    /// Running M2 (sum of squared deviations from the mean).
611    m2: f64,
612}
613
614impl WelfordStats {
615    /// Feed a new sample.
616    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    /// Population variance.
625    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    /// Sample variance (Bessel-corrected).
633    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    /// Standard deviation (population).
641    pub fn std_dev(&self) -> f64 {
642        self.variance().sqrt()
643    }
644}
645
646// ---------------------------------------------------------------------------
647// 14. Parallel histogram reduce (multi-bin merge pattern)
648// ---------------------------------------------------------------------------
649
650/// Parallel histogram reduce: split `data` into `n_workers` chunks, compute
651/// a partial histogram per chunk (in parallel), then merge all partial
652/// histograms serially.  Mirrors the GPU pattern of per-work-group private
653/// histograms followed by a reduction pass.
654pub 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    // Merge
682    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
691// ---------------------------------------------------------------------------
692// 15. Prefix-sum on integer counts (used for compaction offsets)
693// ---------------------------------------------------------------------------
694
695/// Exclusive prefix sum on a `u64` slice.  Returns a new vec.
696pub 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
706/// Inclusive prefix sum on a `u64` slice.
707pub 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
717// ---------------------------------------------------------------------------
718// 16. Tile-based convolution (1-D, CPU mock)
719// ---------------------------------------------------------------------------
720
721/// 1-D convolution of `signal` with `kernel` (full output, length = signal+kernel-1).
722///
723/// This CPU mock mimics the tiled convolution pattern used in GPU compute
724/// shaders where each work-group processes one tile with halo elements.
725pub 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
739/// 1-D cross-correlation of `signal` with `pattern` (valid region only).
740/// Output length = `signal.len() - pattern.len() + 1`.
741pub 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// ---------------------------------------------------------------------------
758// Tests
759// ---------------------------------------------------------------------------
760
761#[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        // Design: 64 threads, 32 regs, 0 smem. With SM supporting 2048 threads,
871        // 64 work-groups limit = 2048/64 = 32. Occupancy = 1.0.
872        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        // Variance = E[(X-mean)^2] = (4+1+0+1+4)/5 = 2.0
886        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    // ── Histogram tests ──────────────────────────────────────────────────
898
899    #[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        // bin 0: [0,1) => 0.1, 0.5, 0.9  count=3
904        // bin 1: [1,2) => 1.5, 1.9        count=2
905        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); // bin 0 has 3 elements
915    }
916
917    #[test]
918    fn test_histogram_bin_centre() {
919        let h = Histogram::compute(&[], 0.0, 4.0, 4);
920        // each bin width = 1.0, centre of bin 0 = 0.5
921        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        // All data in one bin centred at 0.5
928        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    // ── Norm tests ───────────────────────────────────────────────────────
934
935    #[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    // ── Covariance tests ─────────────────────────────────────────────────
961
962    #[test]
963    fn test_covariance_identity_pattern() {
964        // Two variables, perfectly correlated: data = [(0,0),(1,1),(2,2)]
965        // Cov = [[var_x, cov_xy],[cov_yx, var_y]]
966        let data = vec![0.0, 0.0, 1.0, 1.0, 2.0, 2.0];
967        let cov = covariance_matrix(&data, 3, 2);
968        // variance of x = variance of y = 2/3; covariance = 2/3
969        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]; // 2×2
990        let diag = matrix_diagonal(&mat, 2);
991        assert_eq!(diag, vec![1.0, 4.0]);
992    }
993
994    // ── GEMV / GEMM tests ────────────────────────────────────────────────
995
996    #[test]
997    fn test_matvec_identity() {
998        let identity = vec![1.0, 0.0, 0.0, 1.0]; // 2×2 identity
999        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        // A = [[1,2],[3,4]], x = [1,1] => y = [3,7]
1007        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        // A = [[1,2],[3,4]], B = [[5,6],[7,8]]
1017        // C = [[1*5+2*7, 1*6+2*8],[3*5+4*7, 3*6+4*8]] = [[19,22],[43,50]]
1018        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    // ── WelfordStats tests ───────────────────────────────────────────────
1028
1029    #[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        // population variance = 4.0
1037        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    // ── Parallel histogram tests ─────────────────────────────────────────
1049
1050    #[test]
1051    fn test_parallel_histogram_matches_serial() {
1052        let data: Vec<f64> = (0..200).map(|i| i as f64 / 10.0).collect(); // 0.0 .. 19.9
1053        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    // ── Integer scan tests ───────────────────────────────────────────────
1062
1063    #[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    // ── Convolution tests ────────────────────────────────────────────────
1078
1079    #[test]
1080    fn test_convolve1d_basic() {
1081        // [1,2,3] * [0,1,0] = [1,2,3] (identity-ish kernel, padded)
1082        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        // Box filter [1/3, 1/3, 1/3] on [1,2,3,4,5]
1091        let sig = vec![0.0, 6.0, 0.0]; // impulse at centre → box response
1092        let ker = vec![1.0, 1.0, 1.0];
1093        let out = convolve1d(&sig, &ker); // len = 5
1094        // out = [0, 6, 6, 6, 0]
1095        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]; // difference kernel
1105        let out = correlate1d_valid(&sig, &pat);
1106        // [1-3, 2-4, 3-5] = [-2, -2, -2]
1107        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
1114// ---------------------------------------------------------------------------
1115// 17. Blelloch parallel prefix scan (work-efficient)
1116// ---------------------------------------------------------------------------
1117
1118/// Blelloch work-efficient parallel prefix scan (exclusive, in-place).
1119///
1120/// Implements the classic two-phase up-sweep / down-sweep algorithm.
1121/// Operates on a power-of-two sized buffer; the input is padded with zeros
1122/// if its length is not a power of two.
1123///
1124/// # Reference
1125/// Blelloch, G. E. (1990). *Prefix sums and their applications*.
1126pub fn blelloch_exclusive_scan(data: &[f64]) -> Vec<f64> {
1127    if data.is_empty() {
1128        return vec![];
1129    }
1130    // Pad to next power of two
1131    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    // Up-sweep (reduce)
1140    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    // Clear the last element (identity for +)
1152    buf[p - 1] = 0.0;
1153
1154    // Down-sweep
1155    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
1171/// Blelloch inclusive scan: built on top of the exclusive scan.
1172pub 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
1180// ---------------------------------------------------------------------------
1181// 18. Segmented scan (parallel, Blelloch-style)
1182// ---------------------------------------------------------------------------
1183
1184/// Segmented exclusive scan using a parallel Blelloch-style approach.
1185///
1186/// `flags[i] == true` marks the start of a new segment.  Within each segment
1187/// the exclusive prefix sum is computed independently.
1188pub fn blelloch_segmented_exclusive_scan(data: &[f64], flags: &[bool]) -> Vec<f64> {
1189    assert_eq!(data.len(), flags.len());
1190    // We use a simple serial approach that is semantically equivalent to the
1191    // parallel GPU version with predicate propagation.
1192    segmented_exclusive_scan(data, flags)
1193}
1194
1195/// Segmented reduce: parallel version using Rayon.
1196///
1197/// Returns one aggregate per segment.
1198pub fn parallel_segmented_reduce_sum(data: &[f64], flags: &[bool]) -> Vec<f64> {
1199    assert_eq!(data.len(), flags.len());
1200    // Build segment boundaries
1201    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
1214// ---------------------------------------------------------------------------
1215// 19. Stream compaction with index output
1216// ---------------------------------------------------------------------------
1217
1218/// Stream compaction with index tracking.
1219///
1220/// Returns `(compacted_values, original_indices)` — the values that pass
1221/// `predicate` and the indices they came from.
1222pub 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
1237/// Stream compaction with count: returns (compacted, n_removed).
1238pub 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
1247// ---------------------------------------------------------------------------
1248// 20. Radix sort step (single digit / pass)
1249// ---------------------------------------------------------------------------
1250
1251/// Single-pass radix sort step: sort `data` by a single `bit_offset`-wide
1252/// digit extracted at bit position `bit_pos` with radix `radix` (must be a
1253/// power of two, e.g. 256 for 8-bit digits).
1254///
1255/// Returns a new sorted vec.  `key_fn` maps each element to its sort key.
1256pub 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    // Count
1260    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    // Exclusive prefix sum of counts
1266    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    // Scatter
1269    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
1278/// Full 64-bit radix sort (8 passes of 8-bit digits).
1279pub 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
1287/// Radix sort for f64 values (sorts by bit representation, handles sign bit).
1288///
1289/// Uses the standard trick of flipping the sign bit (and all bits for negative
1290/// numbers) so that the radix sort on the bit pattern produces correct order.
1291pub 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) // positive: flip sign bit
1298            } else {
1299                !bits // negative: flip all bits
1300            }
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) // was positive
1308            } else {
1309                !bits // was negative
1310            };
1311            f64::from_bits(recovered)
1312        })
1313        .collect()
1314}
1315
1316// ---------------------------------------------------------------------------
1317// 21. Work-efficient parallel reduce (tree reduction)
1318// ---------------------------------------------------------------------------
1319
1320/// Work-efficient tree reduction: sums `data` using a binary tree pattern.
1321///
1322/// This simulates the GPU tree-reduction kernel where each thread handles one
1323/// element and the active thread count halves each step.
1324pub 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
1343/// Work-efficient tree reduction for max.
1344pub 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
1363/// Work-efficient tree reduction for min.
1364pub 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
1383// ---------------------------------------------------------------------------
1384// 22. Reduce-then-broadcast (GPU idiom)
1385// ---------------------------------------------------------------------------
1386
1387/// Reduce `data` to a scalar and broadcast the result back to all positions.
1388///
1389/// Mimics the GPU pattern: reduce in shared memory → broadcast from lane 0.
1390pub fn reduce_broadcast(data: &[f64]) -> Vec<f64> {
1391    let total: f64 = data.iter().copied().sum();
1392    vec![total; data.len()]
1393}
1394
1395/// Normalise: divide each element by the total sum.
1396pub 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// ---------------------------------------------------------------------------
1405// 23. Multi-level histogram reduce
1406// ---------------------------------------------------------------------------
1407
1408/// Two-level histogram: first pass per-tile, second pass merge.
1409///
1410/// Returns the merged bin counts.
1411#[derive(Debug, Clone)]
1412pub struct TwoLevelHistogram {
1413    /// Merged histogram bins.
1414    pub bins: Vec<u64>,
1415    /// Lower bound of the value range.
1416    pub lo: f64,
1417    /// Upper bound of the value range.
1418    pub hi: f64,
1419    /// Number of work-groups / tiles used.
1420    pub n_tiles: usize,
1421}
1422
1423impl TwoLevelHistogram {
1424    /// Compute a two-level histogram.
1425    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    /// Total count of elements.
1437    pub fn total(&self) -> u64 {
1438        self.bins.iter().sum()
1439    }
1440
1441    /// Compute the approximate median from bin centres.
1442    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// ---------------------------------------------------------------------------
1462// 24. Running min/max tracker (streaming GPU readback)
1463// ---------------------------------------------------------------------------
1464
1465/// Streaming min/max tracker suitable for GPU readback values.
1466#[derive(Debug, Clone, Default)]
1467pub struct RunningMinMax {
1468    /// Current minimum.
1469    pub min: f64,
1470    /// Current maximum.
1471    pub max: f64,
1472    /// Number of samples observed.
1473    pub count: u64,
1474}
1475
1476impl RunningMinMax {
1477    /// Create a new tracker.
1478    pub fn new() -> Self {
1479        Self {
1480            min: f64::INFINITY,
1481            max: f64::NEG_INFINITY,
1482            count: 0,
1483        }
1484    }
1485
1486    /// Update with a new sample.
1487    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    /// Update with a batch of samples.
1494    pub fn update_slice(&mut self, data: &[f64]) {
1495        for &v in data {
1496            self.update(v);
1497        }
1498    }
1499
1500    /// Range (max - min).
1501    pub fn range(&self) -> f64 {
1502        if self.count == 0 {
1503            return 0.0;
1504        }
1505        self.max - self.min
1506    }
1507}
1508
1509// ---------------------------------------------------------------------------
1510// 25. Compact scatter (GPU stream compaction output pattern)
1511// ---------------------------------------------------------------------------
1512
1513/// Compact scatter: given a predicate mask, scatter `src` elements into a
1514/// destination buffer at compacted positions.
1515///
1516/// Returns the number of elements written.
1517pub 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
1528/// Build a compaction offset table from a boolean mask.
1529///
1530/// Returns a vec of length `mask.len()` where `offsets[i]` is the compacted
1531/// index for element `i`, or `usize::MAX` when `mask[i]` is false.
1532pub 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// ---------------------------------------------------------------------------
1545// Tests — new additions
1546// ---------------------------------------------------------------------------
1547
1548#[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    // ── Blelloch scan ────────────────────────────────────────────────────
1576
1577    #[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]; // len=5, not power of 2
1598        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    // ── Segmented parallel reduce ────────────────────────────────────────
1628
1629    #[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    // ── filter_compact_indexed ───────────────────────────────────────────
1648
1649    #[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    // ── Radix sort ───────────────────────────────────────────────────────
1674
1675    #[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        // Sort by lowest byte
1716        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        // Lowest bytes should be non-decreasing
1720        for w in sorted.windows(2) {
1721            assert!(w[0] & 0xFF <= w[1] & 0xFF, "not sorted by low byte");
1722        }
1723    }
1724
1725    // ── Tree reduce ──────────────────────────────────────────────────────
1726
1727    #[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    // ── Reduce + broadcast ───────────────────────────────────────────────
1780
1781    #[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    // ── TwoLevelHistogram ────────────────────────────────────────────────
1807
1808    #[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        // Uniform [0,10) → median ≈ 5.0
1818        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    // ── RunningMinMax ────────────────────────────────────────────────────
1835
1836    #[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    // ── compact_scatter / compaction_offsets ─────────────────────────────
1862
1863    #[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    // ── Additional histogram ──────────────────────────────────────────────
1901
1902    #[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]; // -5 below lo, 15 above hi
1914        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    // ── WelfordStats extended ────────────────────────────────────────────
1923
1924    #[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        // sample variance = (mean_sq_diff) / (n-1) = 2.0 / 1 = 2.0
1930        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    // ── Prefix sum u64 (additional) ───────────────────────────────────────
1948
1949    #[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    // ── Tile operations ───────────────────────────────────────────────────
1962
1963    #[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); // ceil(100/16)
1976    }
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}