Skip to main content

entrenar/efficiency/benchmark/
entry.rs

1//! Benchmark entry type
2
3use crate::efficiency::{ComputeDevice, CostMetrics, EnergyMetrics, ModelParadigm};
4use serde::{Deserialize, Serialize};
5
6/// A single benchmark entry
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct BenchmarkEntry {
9    /// Unique run identifier
10    pub run_id: String,
11    /// Model paradigm used
12    pub paradigm: ModelParadigm,
13    /// Compute device used
14    pub device: ComputeDevice,
15    /// Quality score achieved (accuracy, F1, etc.)
16    pub quality_score: f64,
17    /// Cost metrics
18    pub cost: CostMetrics,
19    /// Energy metrics
20    pub energy: EnergyMetrics,
21}
22
23impl BenchmarkEntry {
24    /// Create a new benchmark entry
25    pub fn new(
26        run_id: impl Into<String>,
27        paradigm: ModelParadigm,
28        device: ComputeDevice,
29        quality_score: f64,
30        cost: CostMetrics,
31        energy: EnergyMetrics,
32    ) -> Self {
33        Self { run_id: run_id.into(), paradigm, device, quality_score, cost, energy }
34    }
35
36    /// Get efficiency score (quality per dollar)
37    pub fn efficiency_score(&self) -> f64 {
38        if self.cost.total_cost_usd > 0.0 {
39            self.quality_score / self.cost.total_cost_usd
40        } else {
41            f64::INFINITY
42        }
43    }
44
45    /// Get energy efficiency (quality per kWh)
46    pub fn energy_efficiency(&self) -> f64 {
47        let kwh = self.energy.kwh();
48        if kwh > 0.0 {
49            self.quality_score / kwh
50        } else {
51            f64::INFINITY
52        }
53    }
54
55    /// Get carbon efficiency (quality per kg CO2)
56    pub fn carbon_efficiency(&self) -> f64 {
57        if self.energy.carbon_kg > 0.0 {
58            self.quality_score / self.energy.carbon_kg
59        } else {
60            f64::INFINITY
61        }
62    }
63
64    /// Check if this entry dominates another (better in all metrics)
65    pub fn dominates(&self, other: &Self) -> bool {
66        self.quality_score >= other.quality_score
67            && self.cost.total_cost_usd <= other.cost.total_cost_usd
68            && self.energy.joules_total <= other.energy.joules_total
69            && (self.quality_score > other.quality_score
70                || self.cost.total_cost_usd < other.cost.total_cost_usd
71                || self.energy.joules_total < other.energy.joules_total)
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::efficiency::device::CpuInfo;
79
80    fn make_entry(quality: f64, cost_usd: f64, joules: f64, carbon: f64) -> BenchmarkEntry {
81        let mut energy = EnergyMetrics::new(100.0, joules, 1000);
82        energy.carbon_kg = carbon;
83        BenchmarkEntry::new(
84            "test-run",
85            ModelParadigm::DeepLearning,
86            ComputeDevice::Cpu(CpuInfo::detect()),
87            quality,
88            CostMetrics {
89                total_cost_usd: cost_usd,
90                cost_per_sample_usd: cost_usd / 1000.0,
91                cost_per_epoch_usd: cost_usd / 10.0,
92                device_hours: 1.0,
93                rate_per_hour_usd: cost_usd,
94            },
95            energy,
96        )
97    }
98
99    #[test]
100    fn test_benchmark_entry_new() {
101        let entry = make_entry(0.95, 10.0, 36000.0, 0.5);
102        assert_eq!(entry.run_id, "test-run");
103        assert_eq!(entry.paradigm, ModelParadigm::DeepLearning);
104        assert!((entry.quality_score - 0.95).abs() < 1e-9);
105        assert!((entry.cost.total_cost_usd - 10.0).abs() < 1e-9);
106    }
107
108    #[test]
109    fn test_efficiency_score_normal() {
110        let entry = make_entry(0.9, 10.0, 36000.0, 0.5);
111        assert!((entry.efficiency_score() - 0.09).abs() < 1e-9);
112    }
113
114    #[test]
115    fn test_efficiency_score_zero_cost() {
116        let entry = make_entry(0.9, 0.0, 36000.0, 0.5);
117        assert!(entry.efficiency_score().is_infinite());
118    }
119
120    #[test]
121    fn test_energy_efficiency_normal() {
122        let entry = make_entry(0.9, 10.0, 3600000.0, 0.5);
123        // 3600000 joules = 1 kWh, so efficiency = 0.9 / 1.0 = 0.9
124        assert!((entry.energy_efficiency() - 0.9).abs() < 1e-9);
125    }
126
127    #[test]
128    fn test_energy_efficiency_zero_energy() {
129        let entry = make_entry(0.9, 10.0, 0.0, 0.5);
130        assert!(entry.energy_efficiency().is_infinite());
131    }
132
133    #[test]
134    fn test_carbon_efficiency_normal() {
135        let entry = make_entry(0.9, 10.0, 36000.0, 0.5);
136        // efficiency = 0.9 / 0.5 = 1.8
137        assert!((entry.carbon_efficiency() - 1.8).abs() < 1e-9);
138    }
139
140    #[test]
141    fn test_carbon_efficiency_zero_carbon() {
142        let entry = make_entry(0.9, 10.0, 36000.0, 0.0);
143        assert!(entry.carbon_efficiency().is_infinite());
144    }
145
146    #[test]
147    fn test_dominates_better_quality() {
148        let better = make_entry(0.95, 10.0, 36000.0, 0.5);
149        let worse = make_entry(0.90, 10.0, 36000.0, 0.5);
150        assert!(better.dominates(&worse));
151        assert!(!worse.dominates(&better));
152    }
153
154    #[test]
155    fn test_dominates_lower_cost() {
156        let better = make_entry(0.9, 5.0, 36000.0, 0.5);
157        let worse = make_entry(0.9, 10.0, 36000.0, 0.5);
158        assert!(better.dominates(&worse));
159        assert!(!worse.dominates(&better));
160    }
161
162    #[test]
163    fn test_dominates_lower_energy() {
164        let better = make_entry(0.9, 10.0, 18000.0, 0.5);
165        let worse = make_entry(0.9, 10.0, 36000.0, 0.5);
166        assert!(better.dominates(&worse));
167        assert!(!worse.dominates(&better));
168    }
169
170    #[test]
171    fn test_dominates_equal_entries() {
172        let entry1 = make_entry(0.9, 10.0, 36000.0, 0.5);
173        let entry2 = make_entry(0.9, 10.0, 36000.0, 0.5);
174        // Equal entries don't dominate each other (need strict improvement in at least one)
175        assert!(!entry1.dominates(&entry2));
176        assert!(!entry2.dominates(&entry1));
177    }
178
179    #[test]
180    fn test_dominates_tradeoff() {
181        // Better quality but worse cost - neither dominates
182        let entry1 = make_entry(0.95, 20.0, 36000.0, 0.5);
183        let entry2 = make_entry(0.90, 10.0, 36000.0, 0.5);
184        assert!(!entry1.dominates(&entry2));
185        assert!(!entry2.dominates(&entry1));
186    }
187
188    #[test]
189    fn test_benchmark_entry_clone() {
190        let entry = make_entry(0.9, 10.0, 36000.0, 0.5);
191        let cloned = entry.clone();
192        assert_eq!(entry, cloned);
193    }
194
195    #[test]
196    fn test_benchmark_entry_serde() {
197        let entry = make_entry(0.9, 10.0, 36000.0, 0.5);
198        let json = serde_json::to_string(&entry).expect("JSON serialization should succeed");
199        let deserialized: BenchmarkEntry =
200            serde_json::from_str(&json).expect("JSON deserialization should succeed");
201        assert_eq!(entry.run_id, deserialized.run_id);
202        assert!((entry.quality_score - deserialized.quality_score).abs() < 1e-9);
203    }
204
205    #[test]
206    fn test_benchmark_entry_debug() {
207        let entry = make_entry(0.9, 10.0, 36000.0, 0.5);
208        let debug_str = format!("{entry:?}");
209        assert!(debug_str.contains("BenchmarkEntry"));
210        assert!(debug_str.contains("test-run"));
211    }
212}