updated docs for release

This commit is contained in:
2026-02-26 17:17:57 +01:00
parent 667d94af7a
commit f4656619be
10 changed files with 335 additions and 81 deletions

View File

@@ -1,7 +1,16 @@
//! 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;
pub mod formatters;
/// A single, atomic data point captured during the benchmark.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ThermalPoint {
pub power_w: f32,
@@ -11,34 +20,53 @@ pub struct ThermalPoint {
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<ThermalPoint>,
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,
pub config_paths: std::collections::HashMap<String, std::path::PathBuf>,
/// A map of configuration files that were written to.
pub config_paths: HashMap<String, PathBuf>,
}
/// 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<f32> {
if data.is_empty() { return vec![]; }
let mut smoothed = Vec::with_capacity(data.len());
@@ -47,7 +75,6 @@ impl OptimizerEngine {
let start = if i < self.window_size { 0 } else { i - self.window_size + 1 };
let end = i + 1;
// Outlier rejection: only average values within a reasonable range
let window = &data[start..end];
let avg: f32 = window.iter().sum::<f32>() / window.len() as f32;
let filtered: Vec<f32> = window.iter()
@@ -63,7 +90,10 @@ impl OptimizerEngine {
smoothed
}
/// Calculates Thermal Resistance: R_theta = (T_core - T_ambient) / P_package
/// 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
@@ -72,6 +102,7 @@ impl OptimizerEngine {
.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)
@@ -79,8 +110,16 @@ impl OptimizerEngine {
.unwrap_or(0.0)
}
/// Finds the "Silicon Knee" - the point where performance per watt (efficiency)
/// 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
@@ -103,8 +142,7 @@ impl OptimizerEngine {
let curr = &points[i];
let next = &points[i + 1];
// 1. Efficiency Metric (Throughput per Watt)
// If throughput is 0 (unsupported), fallback to Frequency per Watt
// 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 {
@@ -117,7 +155,6 @@ impl OptimizerEngine {
next.freq_mhz / next.power_w.max(1.0)
};
// Diminishing returns: how much efficiency drops per additional watt
let p_delta = (next.power_w - curr.power_w).max(0.5);
let efficiency_drop = (efficiency_curr - efficiency_next) / p_delta;
@@ -131,13 +168,10 @@ impl OptimizerEngine {
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 frequency/throughput is a hard wall)
// 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 };
// Heuristic scoring:
// - Higher score is "Better" (The Knee is the peak of this curve)
// - We want high efficiency (low drop) and low thermal acceleration.
let score = (efficiency_curr * 10.0) - (efficiency_drop * 50.0) - (temp_accel * 20.0) - penalty;
if score > max_score {