lmn_core/histogram/
numeric.rs1use rand::Rng;
2use rand::SeedableRng;
3
4use crate::stats::Distribution;
5
6pub struct NumericHistogramParams {
10 pub max_samples: usize,
13}
14
15pub struct NumericHistogram {
26 samples: Vec<f64>,
27 max_samples: usize,
28 total_seen: usize,
29 rng: rand::rngs::SmallRng,
30}
31
32impl NumericHistogram {
33 pub fn new(params: NumericHistogramParams) -> Self {
35 Self {
36 samples: Vec::new(),
37 max_samples: params.max_samples,
38 total_seen: 0,
39 rng: rand::rngs::SmallRng::from_os_rng(),
40 }
41 }
42
43 pub fn record(&mut self, value: f64) {
45 self.total_seen += 1;
46 if self.samples.len() < self.max_samples {
47 self.samples.push(value);
48 } else {
49 let j = self.rng.random_range(0..self.total_seen);
50 if j < self.max_samples {
51 self.samples[j] = value;
52 }
53 }
54 }
55
56 pub fn distribution(&self) -> Distribution {
58 Distribution::from_unsorted(self.samples.clone())
59 }
60
61 pub fn is_empty(&self) -> bool {
63 self.samples.is_empty()
64 }
65
66 pub fn total_seen(&self) -> usize {
68 self.total_seen
69 }
70}
71
72#[cfg(test)]
75mod tests {
76 use super::*;
77
78 fn hist_with_cap(cap: usize) -> NumericHistogram {
79 NumericHistogram::new(NumericHistogramParams { max_samples: cap })
80 }
81
82 #[test]
83 fn record_fills_up_to_cap() {
84 let mut h = hist_with_cap(5);
85 for i in 0..5 {
86 h.record(i as f64);
87 }
88 assert_eq!(h.samples.len(), 5);
89 }
90
91 #[test]
92 fn record_beyond_cap_does_not_grow_vec() {
93 let mut h = hist_with_cap(3);
94 for i in 0..100 {
95 h.record(i as f64);
96 }
97 assert_eq!(h.samples.len(), 3);
98 }
99
100 #[test]
101 fn total_seen_always_increments() {
102 let mut h = hist_with_cap(2);
103 for i in 0..10 {
104 h.record(i as f64);
105 }
106 assert_eq!(h.total_seen(), 10);
107 }
108
109 #[test]
110 fn distribution_returns_correct_quantiles() {
111 let mut h = hist_with_cap(100);
112 for i in 1..=100 {
114 h.record(i as f64);
115 }
116 let dist = h.distribution();
117 assert_eq!(dist.min(), 1.0);
119 assert_eq!(dist.max(), 100.0);
120 let p50 = dist.quantile(0.5);
122 assert!((1.0..=100.0).contains(&p50), "p50={p50} out of range");
123 }
124
125 #[test]
126 fn is_empty_before_records() {
127 let h = hist_with_cap(10);
128 assert!(h.is_empty());
129 }
130}