1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RDPoint {
12 pub codec: String,
14
15 pub quality_setting: f64,
17
18 pub bpp: f64,
20
21 pub quality: f64,
24
25 pub encode_time_ms: Option<f64>,
27
28 pub image: Option<String>,
30}
31
32impl RDPoint {
33 #[must_use]
35 pub fn new(codec: impl Into<String>, quality_setting: f64, bpp: f64, quality: f64) -> Self {
36 Self {
37 codec: codec.into(),
38 quality_setting,
39 bpp,
40 quality,
41 encode_time_ms: None,
42 image: None,
43 }
44 }
45
46 #[must_use]
55 pub fn dominates(&self, other: &Self) -> bool {
56 let better_or_equal_bpp = self.bpp <= other.bpp;
57 let better_or_equal_quality = self.quality >= other.quality;
58 let strictly_better = self.bpp < other.bpp || self.quality > other.quality;
59
60 better_or_equal_bpp && better_or_equal_quality && strictly_better
61 }
62}
63
64#[derive(Debug, Clone, Default, Serialize, Deserialize)]
66pub struct ParetoFront {
67 pub points: Vec<RDPoint>,
69}
70
71impl ParetoFront {
72 #[must_use]
76 pub fn compute(points: &[RDPoint]) -> Self {
77 let mut front = Vec::new();
78
79 for point in points {
80 let is_dominated = front.iter().any(|p: &RDPoint| p.dominates(point));
82
83 if !is_dominated {
84 front.retain(|p| !point.dominates(p));
86 front.push(point.clone());
87 }
88 }
89
90 front.sort_by(|a, b| {
92 a.bpp
93 .partial_cmp(&b.bpp)
94 .unwrap_or(std::cmp::Ordering::Equal)
95 });
96
97 Self { points: front }
98 }
99
100 #[must_use]
102 pub fn is_empty(&self) -> bool {
103 self.points.is_empty()
104 }
105
106 #[must_use]
108 pub fn len(&self) -> usize {
109 self.points.len()
110 }
111
112 #[must_use]
114 pub fn at_quality(&self, min_quality: f64) -> Vec<&RDPoint> {
115 self.points
116 .iter()
117 .filter(|p| p.quality >= min_quality)
118 .collect()
119 }
120
121 #[must_use]
123 pub fn at_bpp(&self, max_bpp: f64) -> Vec<&RDPoint> {
124 self.points.iter().filter(|p| p.bpp <= max_bpp).collect()
125 }
126
127 #[must_use]
129 pub fn best_at_bpp(&self, max_bpp: f64) -> Option<&RDPoint> {
130 self.points
131 .iter()
132 .filter(|p| p.bpp <= max_bpp)
133 .max_by(|a, b| {
134 a.quality
135 .partial_cmp(&b.quality)
136 .unwrap_or(std::cmp::Ordering::Equal)
137 })
138 }
139
140 #[must_use]
142 pub fn best_at_quality(&self, min_quality: f64) -> Option<&RDPoint> {
143 self.points
144 .iter()
145 .filter(|p| p.quality >= min_quality)
146 .min_by(|a, b| {
147 a.bpp
148 .partial_cmp(&b.bpp)
149 .unwrap_or(std::cmp::Ordering::Equal)
150 })
151 }
152
153 #[must_use]
155 pub fn codecs(&self) -> Vec<&str> {
156 let mut codecs: Vec<&str> = self.points.iter().map(|p| p.codec.as_str()).collect();
157 codecs.sort();
158 codecs.dedup();
159 codecs
160 }
161
162 #[must_use]
164 pub fn filter_codec(&self, codec: &str) -> Vec<&RDPoint> {
165 self.points.iter().filter(|p| p.codec == codec).collect()
166 }
167
168 #[must_use]
170 pub fn per_codec(points: &[RDPoint]) -> std::collections::HashMap<String, ParetoFront> {
171 use std::collections::HashMap;
172
173 let mut by_codec: HashMap<String, Vec<RDPoint>> = HashMap::new();
174
175 for point in points {
176 by_codec
177 .entry(point.codec.clone())
178 .or_default()
179 .push(point.clone());
180 }
181
182 by_codec
183 .into_iter()
184 .map(|(codec, pts)| (codec, Self::compute(&pts)))
185 .collect()
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_dominates() {
195 let p1 = RDPoint::new("a", 80.0, 1.0, 90.0); let p2 = RDPoint::new("b", 80.0, 2.0, 85.0); assert!(p1.dominates(&p2));
200 assert!(!p2.dominates(&p1));
201 }
202
203 #[test]
204 fn test_no_dominance() {
205 let p1 = RDPoint::new("a", 80.0, 1.0, 85.0); let p2 = RDPoint::new("b", 80.0, 2.0, 90.0); assert!(!p1.dominates(&p2));
210 assert!(!p2.dominates(&p1));
211 }
212
213 #[test]
214 fn test_pareto_front_basic() {
215 let points = vec![
216 RDPoint::new("a", 80.0, 1.0, 80.0),
217 RDPoint::new("b", 80.0, 2.0, 90.0),
218 RDPoint::new("c", 80.0, 3.0, 85.0), RDPoint::new("d", 80.0, 0.5, 70.0),
220 ];
221
222 let front = ParetoFront::compute(&points);
223
224 assert_eq!(front.len(), 3);
227
228 let codecs = front.codecs();
229 assert!(codecs.contains(&"a"));
230 assert!(codecs.contains(&"b"));
231 assert!(codecs.contains(&"d"));
232 assert!(!codecs.contains(&"c"));
233 }
234
235 #[test]
236 fn test_pareto_best_at_bpp() {
237 let points = vec![
238 RDPoint::new("a", 80.0, 1.0, 80.0),
239 RDPoint::new("b", 80.0, 2.0, 90.0),
240 RDPoint::new("c", 80.0, 0.5, 70.0),
241 ];
242
243 let front = ParetoFront::compute(&points);
244
245 let best = front.best_at_bpp(1.0).unwrap();
247 assert_eq!(best.codec, "a");
248
249 let best = front.best_at_bpp(2.0).unwrap();
251 assert_eq!(best.codec, "b");
252 }
253
254 #[test]
255 fn test_pareto_best_at_quality() {
256 let points = vec![
257 RDPoint::new("a", 80.0, 1.0, 80.0),
258 RDPoint::new("b", 80.0, 2.0, 90.0),
259 RDPoint::new("c", 80.0, 0.5, 70.0),
260 ];
261
262 let front = ParetoFront::compute(&points);
263
264 let best = front.best_at_quality(80.0).unwrap();
266 assert_eq!(best.codec, "a");
267
268 let best = front.best_at_quality(85.0).unwrap();
270 assert_eq!(best.codec, "b");
271 }
272}