Skip to main content

qualia_core_db/solvers/statistics/
histogram.rs

1//! Histogram binning — fills a caller-owned counts buffer, no allocation.
2//!
3//! Canonical home for equal-width binning. The caller owns the `counts` slice
4//! (its length sets the bin count) and the returned range describes the binning.
5
6use super::descriptive::{max, min};
7
8/// Range/width metadata for a binning pass.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct HistRange {
11    pub min: f64,
12    pub max: f64,
13    pub bin_width: f64,
14}
15
16/// Equal-width histogram into the caller-owned `counts` buffer (zeroed first).
17/// The number of bins is `counts.len()`. Values outside [min, max] are skipped;
18/// a value landing on/above the top edge falls in the last bin. `None` if the
19/// data is empty or `counts` is empty.
20pub fn histogram_into(values: &[f64], counts: &mut [u32]) -> Option<HistRange> {
21    let bins = counts.len();
22    if values.is_empty() || bins == 0 {
23        return None;
24    }
25    let min_v = min(values)?;
26    let max_v = max(values)?;
27    let bin_width = (max_v - min_v) / bins as f64;
28
29    for c in counts.iter_mut() {
30        *c = 0;
31    }
32
33    for &v in values {
34        if v < min_v || v > max_v {
35            continue;
36        }
37        let bin_index = if !bin_width.is_finite() || bin_width <= 0.0 {
38            // Degenerate range (all values equal): everything in bin 0.
39            0
40        } else {
41            let idx = ((v - min_v) / bin_width) as usize;
42            if idx >= bins {
43                bins - 1
44            } else {
45                idx
46            }
47        };
48        counts[bin_index] += 1;
49    }
50
51    Some(HistRange {
52        min: min_v,
53        max: max_v,
54        bin_width,
55    })
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    const EPS: f64 = 1e-12;
62
63    #[test]
64    fn guards_empty() {
65        assert_eq!(histogram_into(&[], &mut [0u32; 4]), None);
66        assert_eq!(histogram_into(&[1.0, 2.0], &mut []), None);
67    }
68
69    #[test]
70    fn bins_uniform_data_and_conserves_count() {
71        let v = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
72        let mut counts = [0u32; 5];
73        let r = histogram_into(&v, &mut counts).unwrap();
74        assert!((r.min - 0.0).abs() < EPS);
75        assert!((r.max - 10.0).abs() < EPS);
76        assert!((r.bin_width - 2.0).abs() < EPS);
77        assert_eq!(counts.iter().sum::<u32>(), v.len() as u32); // every point binned
78    }
79
80    #[test]
81    fn degenerate_all_equal_goes_to_bin_zero() {
82        let v = [5.0, 5.0, 5.0];
83        let mut counts = [0u32; 3];
84        let r = histogram_into(&v, &mut counts).unwrap();
85        assert_eq!(r.bin_width, 0.0);
86        assert_eq!(counts[0], 3);
87    }
88
89    #[test]
90    fn buffer_is_zeroed_first() {
91        let v = [1.0, 2.0];
92        let mut counts = [99u32; 2]; // dirty buffer
93        histogram_into(&v, &mut counts).unwrap();
94        assert_eq!(counts.iter().sum::<u32>(), 2);
95    }
96}