eventcv_core/
analytics.rs1use crate::EventStream;
12
13#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct EventRate {
19 pub starts: Vec<i64>,
21 pub counts: Vec<u64>,
23 pub positive: Vec<u64>,
25 pub negative: Vec<u64>,
27 pub bin_us: i64,
29}
30
31impl EventRate {
32 pub fn per_second(&self) -> Vec<f64> {
38 let seconds = self.bin_us as f64 / 1_000_000.0;
39 self.counts.iter().map(|&n| n as f64 / seconds).collect()
40 }
41
42 pub fn len(&self) -> usize {
44 self.starts.len()
45 }
46
47 pub fn is_empty(&self) -> bool {
49 self.starts.is_empty()
50 }
51}
52
53impl EventStream {
54 pub fn event_rate(&self, bin_us: i64) -> EventRate {
63 let bin_us = bin_us.max(1);
64 let ts = self.ts();
65 let (Some(&t_min), Some(&t_max)) = (ts.iter().min(), ts.iter().max()) else {
66 return EventRate {
67 starts: Vec::new(),
68 counts: Vec::new(),
69 positive: Vec::new(),
70 negative: Vec::new(),
71 bin_us,
72 };
73 };
74
75 let bins = ((t_max - t_min) / bin_us + 1) as usize;
78 let mut counts = vec![0u64; bins];
79 let mut positive = vec![0u64; bins];
80 let mut negative = vec![0u64; bins];
81
82 for (&t, &p) in ts.iter().zip(self.ps()) {
83 let bin = ((t - t_min) / bin_us) as usize;
84 counts[bin] += 1;
85 if p {
86 positive[bin] += 1;
87 } else {
88 negative[bin] += 1;
89 }
90 }
91
92 EventRate {
93 starts: (0..bins).map(|i| t_min + i as i64 * bin_us).collect(),
94 counts,
95 positive,
96 negative,
97 bin_us,
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use crate::{EventStream, EventStreamBuilder};
105
106 fn sample() -> EventStream {
107 let mut builder = EventStreamBuilder::new(4, 4, 0.001);
109 for i in 0..10u16 {
110 builder.push(i % 4, i % 4, i64::from(i) * 10, i % 2 == 0);
111 }
112 builder.build()
113 }
114
115 #[test]
116 fn bins_cover_the_whole_span() {
117 let rate = sample().event_rate(50);
118 assert_eq!(rate.starts, vec![0, 50]);
119 assert_eq!(rate.counts, vec![5, 5]);
120 assert_eq!(rate.counts.iter().sum::<u64>(), 10);
121 }
122
123 #[test]
124 fn polarities_sum_to_the_total() {
125 let rate = sample().event_rate(30);
126 for i in 0..rate.len() {
127 assert_eq!(rate.positive[i] + rate.negative[i], rate.counts[i]);
128 }
129 assert_eq!(rate.positive.iter().sum::<u64>(), 5);
130 assert_eq!(rate.negative.iter().sum::<u64>(), 5);
131 }
132
133 #[test]
134 fn a_trailing_partial_bin_is_still_counted() {
135 let rate = sample().event_rate(40);
137 assert_eq!(rate.len(), 3);
138 assert_eq!(rate.counts, vec![4, 4, 2]);
139 }
140
141 #[test]
142 fn per_second_scales_by_bin_width() {
143 let rate = sample().event_rate(50); assert_eq!(rate.per_second(), vec![100_000.0, 100_000.0]);
145 }
146
147 #[test]
148 fn unsorted_input_bins_identically() {
149 let sorted = sample();
150 let shuffled = sorted.time_scale(-1.0).time_scale(-1.0); assert_eq!(sorted.event_rate(30), shuffled.event_rate(30));
152 }
153
154 #[test]
155 fn zero_bin_width_is_clamped() {
156 let rate = sample().event_rate(0);
157 assert_eq!(rate.bin_us, 1);
158 assert_eq!(rate.counts.iter().sum::<u64>(), 10);
159 }
160
161 #[test]
162 fn empty_stream_has_no_bins() {
163 let empty = EventStreamBuilder::new(4, 4, 0.001).build();
164 let rate = empty.event_rate(100);
165 assert!(rate.is_empty());
166 assert_eq!(rate.len(), 0);
167 assert!(rate.per_second().is_empty());
168 }
169}