use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RDPoint {
pub codec: String,
pub quality_setting: f64,
pub bpp: f64,
pub quality: f64,
pub encode_time_ms: Option<f64>,
pub image: Option<String>,
}
impl RDPoint {
#[must_use]
pub fn new(codec: impl Into<String>, quality_setting: f64, bpp: f64, quality: f64) -> Self {
Self {
codec: codec.into(),
quality_setting,
bpp,
quality,
encode_time_ms: None,
image: None,
}
}
#[must_use]
pub fn dominates(&self, other: &Self) -> bool {
let better_or_equal_bpp = self.bpp <= other.bpp;
let better_or_equal_quality = self.quality >= other.quality;
let strictly_better = self.bpp < other.bpp || self.quality > other.quality;
better_or_equal_bpp && better_or_equal_quality && strictly_better
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ParetoFront {
pub points: Vec<RDPoint>,
}
impl ParetoFront {
#[must_use]
pub fn compute(points: &[RDPoint]) -> Self {
let mut front = Vec::new();
for point in points {
let is_dominated = front.iter().any(|p: &RDPoint| p.dominates(point));
if !is_dominated {
front.retain(|p| !point.dominates(p));
front.push(point.clone());
}
}
front.sort_by(|a, b| {
a.bpp
.partial_cmp(&b.bpp)
.unwrap_or(std::cmp::Ordering::Equal)
});
Self { points: front }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.points.len()
}
#[must_use]
pub fn at_quality(&self, min_quality: f64) -> Vec<&RDPoint> {
self.points
.iter()
.filter(|p| p.quality >= min_quality)
.collect()
}
#[must_use]
pub fn at_bpp(&self, max_bpp: f64) -> Vec<&RDPoint> {
self.points.iter().filter(|p| p.bpp <= max_bpp).collect()
}
#[must_use]
pub fn best_at_bpp(&self, max_bpp: f64) -> Option<&RDPoint> {
self.points
.iter()
.filter(|p| p.bpp <= max_bpp)
.max_by(|a, b| {
a.quality
.partial_cmp(&b.quality)
.unwrap_or(std::cmp::Ordering::Equal)
})
}
#[must_use]
pub fn best_at_quality(&self, min_quality: f64) -> Option<&RDPoint> {
self.points
.iter()
.filter(|p| p.quality >= min_quality)
.min_by(|a, b| {
a.bpp
.partial_cmp(&b.bpp)
.unwrap_or(std::cmp::Ordering::Equal)
})
}
#[must_use]
pub fn codecs(&self) -> Vec<&str> {
let mut codecs: Vec<&str> = self.points.iter().map(|p| p.codec.as_str()).collect();
codecs.sort();
codecs.dedup();
codecs
}
#[must_use]
pub fn filter_codec(&self, codec: &str) -> Vec<&RDPoint> {
self.points.iter().filter(|p| p.codec == codec).collect()
}
#[must_use]
pub fn per_codec(points: &[RDPoint]) -> std::collections::HashMap<String, ParetoFront> {
use std::collections::HashMap;
let mut by_codec: HashMap<String, Vec<RDPoint>> = HashMap::new();
for point in points {
by_codec
.entry(point.codec.clone())
.or_default()
.push(point.clone());
}
by_codec
.into_iter()
.map(|(codec, pts)| (codec, Self::compute(&pts)))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dominates() {
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));
assert!(!p2.dominates(&p1));
}
#[test]
fn test_no_dominance() {
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));
assert!(!p2.dominates(&p1));
}
#[test]
fn test_pareto_front_basic() {
let points = vec![
RDPoint::new("a", 80.0, 1.0, 80.0),
RDPoint::new("b", 80.0, 2.0, 90.0),
RDPoint::new("c", 80.0, 3.0, 85.0), RDPoint::new("d", 80.0, 0.5, 70.0),
];
let front = ParetoFront::compute(&points);
assert_eq!(front.len(), 3);
let codecs = front.codecs();
assert!(codecs.contains(&"a"));
assert!(codecs.contains(&"b"));
assert!(codecs.contains(&"d"));
assert!(!codecs.contains(&"c"));
}
#[test]
fn test_pareto_best_at_bpp() {
let points = vec![
RDPoint::new("a", 80.0, 1.0, 80.0),
RDPoint::new("b", 80.0, 2.0, 90.0),
RDPoint::new("c", 80.0, 0.5, 70.0),
];
let front = ParetoFront::compute(&points);
let best = front.best_at_bpp(1.0).unwrap();
assert_eq!(best.codec, "a");
let best = front.best_at_bpp(2.0).unwrap();
assert_eq!(best.codec, "b");
}
#[test]
fn test_pareto_best_at_quality() {
let points = vec![
RDPoint::new("a", 80.0, 1.0, 80.0),
RDPoint::new("b", 80.0, 2.0, 90.0),
RDPoint::new("c", 80.0, 0.5, 70.0),
];
let front = ParetoFront::compute(&points);
let best = front.best_at_quality(80.0).unwrap();
assert_eq!(best.codec, "a");
let best = front.best_at_quality(85.0).unwrap();
assert_eq!(best.codec, "b");
}
}