1#[cfg(feature = "chart")]
20pub mod chart;
21mod pareto;
22pub mod rd_knee;
23
24#[cfg(feature = "chart")]
25pub use chart::{ChartConfig, ChartPoint, ChartSeries, generate_svg};
26pub use pareto::{ParetoFront, RDPoint};
27pub use rd_knee::{
28 AngleBin, AxisRange, BinScheme, CodecConfig, ConfiguredParetoFront, ConfiguredRDPoint,
29 CorpusAggregate, DualAngleBin, EncodeResult, FixedFrame, NormalizationContext, ParamValue,
30 QualityDirection, RDCalibration, RDKnee, RDPosition, plot_rd_svg,
31};
32
33use serde::{Deserialize, Serialize};
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Summary {
38 pub count: usize,
40 pub mean: f64,
42 pub median: f64,
44 pub std_dev: f64,
46 pub min: f64,
48 pub max: f64,
50 pub p5: f64,
52 pub p25: f64,
54 pub p75: f64,
56 pub p95: f64,
58}
59
60impl Summary {
61 #[must_use]
65 pub fn compute(values: &[f64]) -> Option<Self> {
66 if values.is_empty() {
67 return None;
68 }
69
70 let mut sorted = values.to_vec();
71 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
72
73 let count = sorted.len();
74 let sum: f64 = sorted.iter().sum();
75 let mean = sum / count as f64;
76
77 let variance: f64 = sorted.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / count as f64;
78 let std_dev = variance.sqrt();
79
80 let median = percentile_sorted(&sorted, 0.5);
81 let min = sorted[0];
82 let max = sorted[count - 1];
83
84 Some(Self {
85 count,
86 mean,
87 median,
88 std_dev,
89 min,
90 max,
91 p5: percentile_sorted(&sorted, 0.05),
92 p25: percentile_sorted(&sorted, 0.25),
93 p75: percentile_sorted(&sorted, 0.75),
94 p95: percentile_sorted(&sorted, 0.95),
95 })
96 }
97}
98
99#[must_use]
116pub fn median(values: &[f64]) -> f64 {
117 if values.is_empty() {
118 return 0.0;
119 }
120 let mut sorted = values.to_vec();
121 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
122 let mid = sorted.len() / 2;
123 if sorted.len() % 2 == 0 {
124 (sorted[mid - 1] + sorted[mid]) / 2.0
125 } else {
126 sorted[mid]
127 }
128}
129
130#[must_use]
140pub fn mean(values: &[f64]) -> f64 {
141 if values.is_empty() {
142 return 0.0;
143 }
144 values.iter().sum::<f64>() / values.len() as f64
145}
146
147#[must_use]
160pub fn std_dev(values: &[f64]) -> f64 {
161 if values.len() < 2 {
162 return 0.0;
163 }
164 let m = mean(values);
165 let variance = values.iter().map(|x| (x - m).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
166 variance.sqrt()
167}
168
169#[must_use]
185pub fn percentile(values: &[f64], p: f64) -> f64 {
186 if values.is_empty() {
187 return 0.0;
188 }
189 let mut sorted = values.to_vec();
190 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
191 percentile_sorted(&sorted, p)
192}
193
194#[must_use]
207pub fn percentile_u32(values: &[u32], p: f64) -> u32 {
208 if values.is_empty() {
209 return 0;
210 }
211 let mut sorted = values.to_vec();
212 sorted.sort();
213 let pos = p.clamp(0.0, 1.0) * (sorted.len() - 1) as f64;
214 let lower = pos.floor() as usize;
215 let upper = (lower + 1).min(sorted.len() - 1);
216 let frac = pos - lower as f64;
217 let result = sorted[lower] as f64 * (1.0 - frac) + sorted[upper] as f64 * frac;
218 result.round() as u32
219}
220
221#[must_use]
242pub fn trimmed_mean(values: &[f64], trim_pct: f64) -> f64 {
243 if values.is_empty() {
244 return 0.0;
245 }
246 let mut sorted = values.to_vec();
247 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
248 let trim_count = (sorted.len() as f64 * trim_pct.clamp(0.0, 0.5)) as usize;
249 if trim_count * 2 >= sorted.len() {
250 return median(values);
251 }
252 let trimmed = &sorted[trim_count..sorted.len() - trim_count];
253 mean(trimmed)
254}
255
256#[must_use]
269pub fn iqr(values: &[f64]) -> f64 {
270 percentile(values, 0.75) - percentile(values, 0.25)
271}
272
273fn percentile_sorted(sorted: &[f64], p: f64) -> f64 {
276 if sorted.is_empty() {
277 return 0.0;
278 }
279 if sorted.len() == 1 {
280 return sorted[0];
281 }
282
283 let p = if p > 1.0 { p / 100.0 } else { p };
285 let p = p.clamp(0.0, 1.0);
286
287 let idx = p * (sorted.len() - 1) as f64;
288 let lower = idx.floor() as usize;
289 let upper = idx.ceil() as usize;
290 let frac = idx - lower as f64;
291
292 if lower == upper {
293 sorted[lower]
294 } else {
295 sorted[lower] * (1.0 - frac) + sorted[upper] * frac
296 }
297}
298
299#[must_use]
314pub fn bd_rate(reference: &[(f64, f64)], test: &[(f64, f64)]) -> Option<f64> {
315 if reference.len() < 4 || test.len() < 4 {
316 return None;
317 }
318
319 let mut ref_sorted: Vec<_> = reference.to_vec();
321 let mut test_sorted: Vec<_> = test.to_vec();
322 ref_sorted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
323 test_sorted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
324
325 let min_quality = ref_sorted[0].1.max(test_sorted[0].1);
327 let max_quality = ref_sorted.last()?.1.min(test_sorted.last()?.1);
328
329 if min_quality >= max_quality {
330 return None;
331 }
332
333 let ref_log: Vec<_> = ref_sorted.iter().map(|(r, q)| (r.ln(), *q)).collect();
335 let test_log: Vec<_> = test_sorted.iter().map(|(r, q)| (r.ln(), *q)).collect();
336
337 let ref_area = integrate_curve(&ref_log, min_quality, max_quality);
339 let test_area = integrate_curve(&test_log, min_quality, max_quality);
340
341 let avg_ref = ref_area / (max_quality - min_quality);
342 let avg_test = test_area / (max_quality - min_quality);
343
344 let bd = (10_f64.powf(avg_test - avg_ref) - 1.0) * 100.0;
346
347 Some(bd)
348}
349
350fn integrate_curve(points: &[(f64, f64)], min_x: f64, max_x: f64) -> f64 {
352 let mut area = 0.0;
353
354 for window in points.windows(2) {
355 let (y0, x0) = window[0];
356 let (y1, x1) = window[1];
357
358 if x1 < min_x || x0 > max_x {
360 continue;
361 }
362
363 let x0 = x0.max(min_x);
365 let x1 = x1.min(max_x);
366
367 area += (y0 + y1) / 2.0 * (x1 - x0);
369 }
370
371 area
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_summary_compute() {
380 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
381 let summary = Summary::compute(&values).unwrap();
382
383 assert_eq!(summary.count, 5);
384 assert!((summary.mean - 3.0).abs() < 0.001);
385 assert!((summary.median - 3.0).abs() < 0.001);
386 assert!((summary.min - 1.0).abs() < 0.001);
387 assert!((summary.max - 5.0).abs() < 0.001);
388 }
389
390 #[test]
391 fn test_summary_empty() {
392 assert!(Summary::compute(&[]).is_none());
393 }
394
395 #[test]
396 fn test_percentile() {
397 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
398 assert!((percentile(&values, 0.0) - 1.0).abs() < 0.001);
399 assert!((percentile(&values, 0.5) - 3.0).abs() < 0.001);
400 assert!((percentile(&values, 1.0) - 5.0).abs() < 0.001);
401 }
402
403 #[test]
404 fn test_median() {
405 assert_eq!(median(&[1.0, 2.0, 3.0, 4.0, 5.0]), 3.0);
406 assert_eq!(median(&[1.0, 2.0, 3.0, 4.0]), 2.5);
407 assert_eq!(median(&[5.0]), 5.0);
408 assert_eq!(median(&[]), 0.0);
409 }
410
411 #[test]
412 fn test_trimmed_mean() {
413 let values = [1.0, 10.0, 11.0, 12.0, 13.0, 100.0];
415 let tm = trimmed_mean(&values, 0.2);
416 assert!((tm - 11.5).abs() < 0.001);
417
418 let values2 = [1.0, 2.0, 3.0, 4.0, 5.0];
420 let tm2 = trimmed_mean(&values2, 0.2);
421 assert!((tm2 - 3.0).abs() < 0.001);
422 }
423
424 #[test]
425 fn test_iqr() {
426 let values = [1.0, 2.0, 3.0, 4.0, 5.0];
427 assert!((iqr(&values) - 2.0).abs() < 0.001);
428 }
429
430 #[test]
431 fn test_percentile_u32() {
432 let values = [10, 20, 30, 40, 50];
433 assert_eq!(percentile_u32(&values, 0.0), 10);
434 assert_eq!(percentile_u32(&values, 0.5), 30);
435 assert_eq!(percentile_u32(&values, 1.0), 50);
436 }
437
438 #[test]
439 fn test_bd_rate_same_curve() {
440 let curve = vec![
441 (1000.0, 30.0),
442 (2000.0, 35.0),
443 (4000.0, 40.0),
444 (8000.0, 45.0),
445 ];
446
447 let bd = bd_rate(&curve, &curve);
448 assert!(bd.is_some());
449 assert!(bd.unwrap().abs() < 0.1); }
451}