//! The core mathematics and physics engine for `ember-tune`. //! //! This module contains the `OptimizerEngine`, which is responsible for all //! data smoothing, thermal resistance calculations, and the heuristic scoring //! used to identify the "Silicon Knee". use serde::{Serialize, Deserialize}; use std::collections::HashMap; use std::path::PathBuf; use tracing::warn; pub mod formatters; /// A single, atomic data point captured during the benchmark. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ThermalPoint { pub power_w: f32, pub temp_c: f32, pub freq_mhz: f32, pub fan_rpm: u32, pub throughput: f64, } /// A complete thermal profile containing all data points for a benchmark run. #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct ThermalProfile { pub points: Vec, pub ambient_temp: f32, } /// The final, recommended parameters derived from the thermal benchmark. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OptimizationResult { /// The full thermal profile used for calculations. pub profile: ThermalProfile, /// The power level (in Watts) where performance-per-watt plateaus. pub silicon_knee_watts: f32, /// The measured thermal resistance of the system (Kelvin/Watt). pub thermal_resistance_kw: f32, /// The recommended sustained power limit (PL1). pub recommended_pl1: f32, /// The recommended burst power limit (PL2). pub recommended_pl2: f32, /// The maximum temperature reached during the test. pub max_temp_c: f32, /// Indicates if the benchmark was aborted before completion. pub is_partial: bool, /// A map of configuration files that were written to. pub config_paths: HashMap, /// The comprehensive optimization matrix (Silent, Balanced, Performance). pub optimization_matrix: Option, } /// Pure mathematics engine for thermal optimization. /// /// Contains no hardware I/O and operates solely on the collected [ThermalProfile]. pub struct OptimizerEngine { /// The size of the sliding window for the `smooth` function. window_size: usize, } impl OptimizerEngine { /// Creates a new `OptimizerEngine`. pub fn new(window_size: usize) -> Self { Self { window_size } } /// Applies a simple moving average (SMA) filter with outlier rejection. /// /// This function smooths noisy sensor data. It rejects any value in the /// window that is more than 20.0 units away from the window's average /// before calculating the final smoothed value. pub fn smooth(&self, data: &[f32]) -> Vec { if data.is_empty() { return vec![]; } let mut smoothed = Vec::with_capacity(data.len()); for i in 0..data.len() { let start = if i < self.window_size { 0 } else { i - self.window_size + 1 }; let end = i + 1; let window = &data[start..end]; let avg: f32 = window.iter().sum::() / window.len() as f32; let filtered: Vec = window.iter() .filter(|&&v| (v - avg).abs() < 20.0) // Reject spikes > 20 units .cloned().collect(); if filtered.is_empty() { smoothed.push(avg); } else { smoothed.push(filtered.iter().sum::() / filtered.len() as f32); } } smoothed } /// Calculates Thermal Resistance: R_theta = (T_core - T_ambient) / P_package. /// /// This function uses the data point with the highest power draw to ensure /// the calculation reflects a system under maximum thermal load. pub fn calculate_thermal_resistance(&self, profile: &ThermalProfile) -> f32 { profile.points.iter() .filter(|p| p.power_w > 1.0 && p.temp_c > 30.0) // Filter invalid data .max_by(|a, b| a.power_w.partial_cmp(&b.power_w).unwrap_or(std::cmp::Ordering::Equal)) .map(|p| (p.temp_c - profile.ambient_temp) / p.power_w) .unwrap_or(0.0) } /// Returns the maximum temperature recorded in the profile. pub fn get_max_temp(&self, profile: &ThermalProfile) -> f32 { profile.points.iter() .map(|p| p.temp_c) .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) .unwrap_or(0.0) } /// Finds the "Silicon Knee" - the point where performance-per-watt (efficiency) /// starts to diminish significantly and thermal density spikes. /// /// This heuristic scoring model balances several factors: /// 1. **Efficiency Drop:** How quickly does performance-per-watt decrease as power increases? /// 2. **Thermal Acceleration:** How quickly does temperature rise per additional Watt? /// 3. **Throttling Penalty:** A large penalty is applied if absolute performance drops, indicating a thermal wall. /// /// The "Knee" is the power level with the highest score, representing the optimal /// balance before thermal saturation causes diminishing returns. pub fn find_silicon_knee(&self, profile: &ThermalProfile) -> f32 { let valid_points: Vec<_> = profile.points.iter() .filter(|p| p.power_w > 5.0 && p.temp_c > 40.0) // Filter idle/noise .cloned() .collect(); if valid_points.len() < 3 { return profile.points.last().map(|p| p.power_w).unwrap_or(15.0); } let mut points = valid_points; points.sort_by(|a, b| a.power_w.partial_cmp(&b.power_w).unwrap_or(std::cmp::Ordering::Equal)); let mut best_pl = points[0].power_w; let mut max_score = f32::MIN; // Use a sliding window (3 points) to calculate gradients more robustly for i in 1..points.len() - 1 { let prev = &points[i - 1]; let curr = &points[i]; let next = &points[i + 1]; // 1. Efficiency Metric (Throughput per Watt or Freq per Watt) let efficiency_curr = if curr.throughput > 0.0 { curr.throughput as f32 / curr.power_w.max(1.0) } else { curr.freq_mhz / curr.power_w.max(1.0) }; let efficiency_next = if next.throughput > 0.0 { next.throughput as f32 / next.power_w.max(1.0) } else { next.freq_mhz / next.power_w.max(1.0) }; let p_delta = (next.power_w - curr.power_w).max(0.5); let efficiency_drop = (efficiency_curr - efficiency_next) / p_delta; // 2. Thermal Acceleration (d2T/dW2) let p_delta_prev = (curr.power_w - prev.power_w).max(0.5); let p_delta_next = (next.power_w - curr.power_w).max(0.5); let dt_dw_prev = (curr.temp_c - prev.temp_c) / p_delta_prev; let dt_dw_next = (next.temp_c - curr.temp_c) / p_delta_next; let p_total_delta = (next.power_w - prev.power_w).max(1.0); let temp_accel = (dt_dw_next - dt_dw_prev) / p_total_delta; // 3. Wall Detection (Any drop in absolute performance is a hard wall) let is_throttling = next.freq_mhz < curr.freq_mhz || (next.throughput > 0.0 && next.throughput < curr.throughput); let penalty = if is_throttling { 5000.0 } else { 0.0 }; let score = (efficiency_curr * 10.0) - (efficiency_drop * 50.0) - (temp_accel * 20.0) - penalty; if score > max_score { max_score = score; best_pl = curr.power_w; } } let best_pl = if max_score > f32::MIN { best_pl } else { profile.points.last().map(|p| p.power_w).unwrap_or(15.0) }; // Safety Floor: Never recommend a TDP below 5W, as this bricks system performance. if best_pl < 5.0 { warn!("Heuristic suggested dangerously low PL1 ({:.1}W). Falling back to 15W safety floor.", best_pl); return 15.0; } best_pl } }