llm_optimizer_processor/aggregation/
percentile.rs1use super::trait_::{Aggregator, ToF64};
2use anyhow::anyhow;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct PercentileAccumulator {
8 values: Vec<f64>,
9 percentile: f64,
10}
11
12#[derive(Debug, Clone)]
38pub struct PercentileAggregator<T> {
39 values: Vec<f64>,
40 percentile: f64,
41 _phantom: std::marker::PhantomData<T>,
42}
43
44impl<T: ToF64 + Clone> PercentileAggregator<T> {
45 pub fn new(percentile: f64) -> anyhow::Result<Self> {
47 if !(0.0..=100.0).contains(&percentile) {
48 return Err(anyhow!("Percentile must be between 0 and 100"));
49 }
50 Ok(Self {
51 values: Vec::new(),
52 percentile,
53 _phantom: std::marker::PhantomData,
54 })
55 }
56
57 pub fn p50() -> Self {
59 Self {
60 values: Vec::new(),
61 percentile: 50.0,
62 _phantom: std::marker::PhantomData,
63 }
64 }
65
66 pub fn p95() -> Self {
68 Self {
69 values: Vec::new(),
70 percentile: 95.0,
71 _phantom: std::marker::PhantomData,
72 }
73 }
74
75 pub fn p99() -> Self {
77 Self {
78 values: Vec::new(),
79 percentile: 99.0,
80 _phantom: std::marker::PhantomData,
81 }
82 }
83
84 pub fn p999() -> Self {
86 Self {
87 values: Vec::new(),
88 percentile: 99.9,
89 _phantom: std::marker::PhantomData,
90 }
91 }
92
93 fn calculate_percentile(sorted_values: &[f64], percentile: f64) -> Option<f64> {
95 if sorted_values.is_empty() {
96 return None;
97 }
98
99 if sorted_values.len() == 1 {
100 return Some(sorted_values[0]);
101 }
102
103 let rank = (percentile / 100.0) * (sorted_values.len() - 1) as f64;
104 let lower_idx = rank.floor() as usize;
105 let upper_idx = rank.ceil() as usize;
106
107 if lower_idx == upper_idx {
108 Some(sorted_values[lower_idx])
109 } else {
110 let fraction = rank - lower_idx as f64;
111 Some(sorted_values[lower_idx] * (1.0 - fraction)
112 + sorted_values[upper_idx] * fraction)
113 }
114 }
115}
116
117impl<T: ToF64 + Clone> Default for PercentileAggregator<T> {
118 fn default() -> Self {
119 Self::p50()
120 }
121}
122
123impl<T: ToF64 + Clone + Send + Sync + std::fmt::Debug> Aggregator for PercentileAggregator<T> {
124 type Input = T;
125 type Output = f64;
126 type Accumulator = PercentileAccumulator;
127
128 fn new() -> Self {
129 Self::p50()
130 }
131
132 fn update(&mut self, value: <Self as Aggregator>::Input) -> anyhow::Result<()> {
133 self.values.push(value.to_f64());
134 Ok(())
135 }
136
137 fn finalize(&self) -> anyhow::Result<<Self as Aggregator>::Output> {
138 if self.values.is_empty() {
139 return Err(anyhow!("Cannot compute percentile of zero values"));
140 }
141
142 let mut sorted = self.values.clone();
143 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
144
145 Self::calculate_percentile(&sorted, self.percentile)
146 .ok_or_else(|| anyhow!("Failed to calculate percentile"))
147 }
148
149 fn accumulator(&self) -> <Self as Aggregator>::Accumulator {
150 PercentileAccumulator {
151 values: self.values.clone(),
152 percentile: self.percentile,
153 }
154 }
155
156 fn merge(&mut self, other: <Self as Aggregator>::Accumulator) -> anyhow::Result<()> {
157 if (self.percentile - other.percentile).abs() > f64::EPSILON {
158 return Err(anyhow!(
159 "Cannot merge percentile aggregators with different percentiles: {} vs {}",
160 self.percentile,
161 other.percentile
162 ));
163 }
164 self.values.extend(other.values);
165 Ok(())
166 }
167
168 fn reset(&mut self) {
169 self.values.clear();
170 }
171
172 fn count(&self) -> u64 {
173 self.values.len() as u64
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_percentile_p50() {
183 let mut agg = PercentileAggregator::<f64>::p50();
184 for i in 1..=100 {
185 agg.update(i as f64).unwrap();
186 }
187
188 let p50 = agg.finalize().unwrap();
189 assert!((p50 - 50.5).abs() < 0.1);
190 }
191
192 #[test]
193 fn test_percentile_p95() {
194 let mut agg = PercentileAggregator::<f64>::p95();
195 for i in 1..=100 {
196 agg.update(i as f64).unwrap();
197 }
198
199 let p95 = agg.finalize().unwrap();
200 assert!((p95 - 95.05).abs() < 0.1);
201 }
202
203 #[test]
204 fn test_percentile_p99() {
205 let mut agg = PercentileAggregator::<f64>::p99();
206 for i in 1..=100 {
207 agg.update(i as f64).unwrap();
208 }
209
210 let p99 = agg.finalize().unwrap();
211 assert!((p99 - 99.01).abs() < 0.1);
212 }
213
214 #[test]
215 fn test_percentile_custom() {
216 let mut agg = PercentileAggregator::<f64>::new(75.0).unwrap();
217 for i in 1..=100 {
218 agg.update(i as f64).unwrap();
219 }
220
221 let p75 = agg.finalize().unwrap();
222 assert!((p75 - 75.25).abs() < 0.5);
223 }
224
225 #[test]
226 fn test_percentile_batch() {
227 let mut agg = PercentileAggregator::<f64>::p50();
228 let values: Vec<f64> = (1..=100).map(|i| i as f64).collect();
229 agg.update_batch(&values).unwrap();
230
231 let p50 = agg.finalize().unwrap();
232 assert!((p50 - 50.5).abs() < 0.1);
233 }
234
235 #[test]
236 fn test_percentile_merge() {
237 let mut agg1 = PercentileAggregator::<f64>::p50();
238 for i in 1..=50 {
239 agg1.update(i as f64).unwrap();
240 }
241
242 let mut agg2 = PercentileAggregator::<f64>::p50();
243 for i in 51..=100 {
244 agg2.update(i as f64).unwrap();
245 }
246
247 let acc2 = agg2.accumulator();
248 agg1.merge(acc2).unwrap();
249
250 let p50 = agg1.finalize().unwrap();
251 assert!((p50 - 50.5).abs() < 0.1);
252 assert_eq!(agg1.count(), 100);
253 }
254
255 #[test]
256 fn test_percentile_merge_different_percentiles_fails() {
257 let mut agg1 = PercentileAggregator::<f64>::p50();
258 agg1.update(1.0).unwrap();
259
260 let mut agg2 = PercentileAggregator::<f64>::p95();
261 agg2.update(2.0).unwrap();
262
263 let acc2 = agg2.accumulator();
264 assert!(agg1.merge(acc2).is_err());
265 }
266
267 #[test]
268 fn test_percentile_reset() {
269 let mut agg = PercentileAggregator::<f64>::p50();
270 agg.update(1.0).unwrap();
271 agg.update(2.0).unwrap();
272
273 assert_eq!(agg.count(), 2);
274
275 agg.reset();
276 assert_eq!(agg.count(), 0);
277 assert!(agg.finalize().is_err());
278 }
279
280 #[test]
281 fn test_percentile_serialization() {
282 let mut agg = PercentileAggregator::<f64>::p95();
283 agg.update(10.0).unwrap();
284 agg.update(20.0).unwrap();
285
286 let acc = agg.accumulator();
287 let serialized = serde_json::to_string(&acc).unwrap();
288 let deserialized: PercentileAccumulator = serde_json::from_str(&serialized).unwrap();
289
290 assert_eq!(acc, deserialized);
291 assert_eq!(deserialized.percentile, 95.0);
292 assert_eq!(deserialized.values.len(), 2);
293 }
294
295 #[test]
296 fn test_percentile_empty() {
297 let agg = PercentileAggregator::<f64>::p50();
298 assert!(agg.finalize().is_err());
299 }
300
301 #[test]
302 fn test_percentile_single_value() {
303 let mut agg = PercentileAggregator::<f64>::p95();
304 agg.update(42.0).unwrap();
305
306 assert_eq!(agg.finalize().unwrap(), 42.0);
307 }
308
309 #[test]
310 fn test_percentile_invalid_value() {
311 assert!(PercentileAggregator::<f64>::new(-1.0).is_err());
312 assert!(PercentileAggregator::<f64>::new(101.0).is_err());
313 }
314
315 #[test]
316 fn test_percentile_edge_cases() {
317 let mut agg = PercentileAggregator::<f64>::new(0.0).unwrap();
319 agg.update_batch(&[10.0, 20.0, 30.0]).unwrap();
320 assert_eq!(agg.finalize().unwrap(), 10.0);
321
322 let mut agg = PercentileAggregator::<f64>::new(100.0).unwrap();
324 agg.update_batch(&[10.0, 20.0, 30.0]).unwrap();
325 assert_eq!(agg.finalize().unwrap(), 30.0);
326 }
327
328 #[test]
329 fn test_percentile_with_duplicates() {
330 let mut agg = PercentileAggregator::<f64>::p50();
331 agg.update_batch(&[1.0, 2.0, 2.0, 2.0, 3.0]).unwrap();
332
333 assert_eq!(agg.finalize().unwrap(), 2.0);
335 }
336
337 #[test]
338 fn test_percentile_unsorted_input() {
339 let mut agg = PercentileAggregator::<f64>::p50();
340 agg.update_batch(&[50.0, 10.0, 30.0, 20.0, 40.0]).unwrap();
341
342 assert_eq!(agg.finalize().unwrap(), 30.0);
344 }
345}