use std::fmt::Debug;
#[allow(dead_code)]
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::numeric::Float;
use std::collections::{HashMap, VecDeque};
use crate::error::{OptimError, Result};
#[derive(Debug, Clone)]
pub struct PerformanceEstimatorConfig<T: Float + Debug + Send + Sync + 'static> {
pub min_epochs: usize,
pub early_stop_threshold: T,
pub patience: usize,
pub fitting_method: CurveFittingMethod,
pub confidence_threshold: T,
pub max_prediction_epochs: usize,
pub smoothing_factor: T,
}
#[derive(Debug)]
pub struct PerformanceEstimator<T: Float + Debug + Send + Sync + 'static> {
config: PerformanceEstimatorConfig<T>,
performance_history: HashMap<String, PerformanceHistory<T>>,
fitting_models: HashMap<String, CurveFittingModel<T>>,
early_stop_decisions: HashMap<String, EarlyStopDecision<T>>,
prediction_stats: PredictionStats<T>,
}
#[derive(Debug, Clone)]
pub struct PerformanceHistory<T: Float + Debug + Send + Sync + 'static> {
pub arch_id: String,
pub training_accuracy: Vec<T>,
pub validation_accuracy: Vec<T>,
pub training_loss: Vec<T>,
pub validation_loss: Vec<T>,
pub epochs: Vec<usize>,
pub epoch_times: Vec<T>,
pub memory_usage: Vec<T>,
}
#[derive(Debug, Clone, Copy)]
pub enum CurveFittingMethod {
Exponential,
PowerLaw,
Logarithmic,
Polynomial,
LearningCurve,
NeuralPredictor,
}
#[derive(Debug, Clone)]
pub struct CurveFittingModel<T: Float + Debug + Send + Sync + 'static> {
pub model_type: CurveFittingMethod,
pub parameters: Array1<T>,
pub r_squared: T,
pub confidence: T,
pub complexity: usize,
}
#[derive(Debug, Clone)]
pub struct EarlyStopDecision<T: Float + Debug + Send + Sync + 'static> {
pub should_stop: bool,
pub predicted_performance: T,
pub prediction_confidence: T,
pub stop_reason: StopReason,
pub decision_epoch: usize,
pub estimated_remaining_time: T,
}
#[derive(Debug, Clone)]
pub enum StopReason {
Converged,
Plateau,
Declining,
BelowThreshold,
Unstable,
ResourceConstrained,
}
#[derive(Debug, Clone)]
pub struct PredictionStats<T: Float + Debug + Send + Sync + 'static> {
pub mean_absolute_error: T,
pub rmse: T,
pub correlation: T,
pub successful_predictions: usize,
pub failed_predictions: usize,
pub early_stop_accuracy: T,
}
#[derive(Debug, Clone)]
pub struct LearningCurveParams<T: Float + Debug + Send + Sync + 'static> {
pub initial_performance: T,
pub learning_rate: T,
pub decay_rate: T,
pub asymptotic_performance: T,
pub noise_level: T,
}
#[derive(Debug)]
pub struct NeuralPerformancePredictor<T: Float + Debug + Send + Sync + 'static> {
weights: Vec<Array2<T>>,
biases: Vec<Array1<T>>,
architecture: Vec<usize>,
training_history: Vec<T>,
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> PerformanceEstimator<T> {
pub fn new(config: PerformanceEstimatorConfig<T>) -> Self {
Self {
config,
performance_history: HashMap::new(),
fitting_models: HashMap::new(),
early_stop_decisions: HashMap::new(),
prediction_stats: PredictionStats::new(),
}
}
pub fn update_performance(&mut self, arch_id: &str, epoch: usize,
train_acc: T, val_acc: T, train_loss: T, val_loss: T,
epoch_time: T, memory_usage: T) -> Result<()> {
let history = self.performance_history
.entry(arch_id.to_string())
.or_insert_with(|| PerformanceHistory {
arch_id: arch_id.to_string(),
training_accuracy: Vec::new(),
validation_accuracy: Vec::new(),
training_loss: Vec::new(),
validation_loss: Vec::new(),
epochs: Vec::new(),
epoch_times: Vec::new(),
memory_usage: Vec::new(),
});
history.training_accuracy.push(train_acc);
history.validation_accuracy.push(val_acc);
history.training_loss.push(train_loss);
history.validation_loss.push(val_loss);
history.epochs.push(epoch);
history.epoch_times.push(epoch_time);
history.memory_usage.push(memory_usage);
Ok(())
}
pub fn predict_performance(&mut self, arch_id: &str) -> Result<T> {
let history = self.performance_history.get(arch_id)
.ok_or_else(|| OptimError::InvalidInput(format!("No history for architecture {}", arch_id)))?;
if history.epochs.len() < self.config.min_epochs {
return Err(OptimError::InsufficientData(
"Not enough epochs for prediction".to_string()
));
}
let model = self.fit_curve(arch_id, &history.validation_accuracy)?;
let predicted_performance = self.extrapolate_performance(&model, self.config.max_prediction_epochs)?;
self.fitting_models.insert(arch_id.to_string(), model);
Ok(predicted_performance)
}
pub fn should_stop_early(&mut self, arch_id: &str) -> Result<EarlyStopDecision<T>> {
let history = self.performance_history.get(arch_id)
.ok_or_else(|| OptimError::InvalidInput(format!("No history for architecture {}", arch_id)))?;
if history.epochs.len() < self.config.min_epochs {
return Ok(EarlyStopDecision {
should_stop: false,
predicted_performance: T::zero(),
prediction_confidence: T::zero(),
stop_reason: StopReason::Converged,
decision_epoch: history.epochs.len(),
estimated_remaining_time: T::zero(),
});
}
if self.has_converged(&history.validation_accuracy)? {
let predicted_perf = self.predict_performance(arch_id)?;
return Ok(EarlyStopDecision {
should_stop: true,
predicted_performance: predicted_perf,
prediction_confidence: scirs2_core::numeric::NumCast::from(0.9).unwrap_or_else(|| T::zero()),
stop_reason: StopReason::Converged,
decision_epoch: history.epochs.len(),
estimated_remaining_time: self.estimate_remaining_time(history)?,
});
}
if self.has_plateaued(&history.validation_accuracy)? {
let predicted_perf = self.predict_performance(arch_id)?;
return Ok(EarlyStopDecision {
should_stop: true,
predicted_performance: predicted_perf,
prediction_confidence: scirs2_core::numeric::NumCast::from(0.8).unwrap_or_else(|| T::zero()),
stop_reason: StopReason::Plateau,
decision_epoch: history.epochs.len(),
estimated_remaining_time: self.estimate_remaining_time(history)?,
});
}
if self.is_declining(&history.validation_accuracy)? {
let predicted_perf = *history.validation_accuracy.iter().max_by(|a, b|
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)).unwrap_or(&T::zero());
return Ok(EarlyStopDecision {
should_stop: true,
predicted_performance: predicted_perf,
prediction_confidence: scirs2_core::numeric::NumCast::from(0.7).unwrap_or_else(|| T::zero()),
stop_reason: StopReason::Declining,
decision_epoch: history.epochs.len(),
estimated_remaining_time: self.estimate_remaining_time(history)?,
});
}
let predicted_perf = self.predict_performance(arch_id)?;
if predicted_perf < self.config.early_stop_threshold {
return Ok(EarlyStopDecision {
should_stop: true,
predicted_performance: predicted_perf,
prediction_confidence: scirs2_core::numeric::NumCast::from(0.6).unwrap_or_else(|| T::zero()),
stop_reason: StopReason::BelowThreshold,
decision_epoch: history.epochs.len(),
estimated_remaining_time: self.estimate_remaining_time(history)?,
});
}
Ok(EarlyStopDecision {
should_stop: false,
predicted_performance: predicted_perf,
prediction_confidence: scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::zero()),
stop_reason: StopReason::Converged,
decision_epoch: history.epochs.len(),
estimated_remaining_time: self.estimate_remaining_time(history)?,
})
}
fn fit_curve(&self, arch_id: &str, performance_data: &[T]) -> Result<CurveFittingModel<T>> {
match self.config.fitting_method {
CurveFittingMethod::Exponential => self.fit_exponential(performance_data),
CurveFittingMethod::PowerLaw => self.fit_power_law(performance_data),
CurveFittingMethod::Logarithmic => self.fit_logarithmic(performance_data),
CurveFittingMethod::Polynomial => self.fit_polynomial(performance_data),
CurveFittingMethod::LearningCurve => self.fit_learning_curve(performance_data),
CurveFittingMethod::NeuralPredictor => self.fit_neural_predictor(performance_data),
}
}
fn fit_exponential(&self, data: &[T]) -> Result<CurveFittingModel<T>> {
if data.len() < 3 {
return Err(OptimError::InsufficientData("Need at least 3 data points".to_string()));
}
let n = data.len();
let mut params = Array1::zeros(3);
let y_start = data[0];
let y_end = *data.last().expect("unwrap failed");
let y_range = y_end - y_start;
params[0] = y_range; params[1] = scirs2_core::numeric::NumCast::from(-0.1).unwrap_or_else(|| T::zero()); params[2] = y_start;
let mean_y = data.iter().cloned().fold(T::zero(), |acc, y| acc + y) / scirs2_core::numeric::NumCast::from(n as f64).unwrap_or_else(|| T::zero());
let mut ss_tot = T::zero();
let mut ss_res = T::zero();
for (i, &y) in data.iter().enumerate() {
let x = scirs2_core::numeric::NumCast::from(i as f64).unwrap_or_else(|| T::zero());
let y_pred = params[0] * (params[1] * x).exp() + params[2];
ss_tot = ss_tot + (y - mean_y) * (y - mean_y);
ss_res = ss_res + (y - y_pred) * (y - y_pred);
}
let r_squared = T::one() - (ss_res / ss_tot);
Ok(CurveFittingModel {
model_type: CurveFittingMethod::Exponential,
parameters: params,
r_squared: r_squared.max(T::zero()),
confidence: r_squared.max(T::zero()),
complexity: 3,
})
}
fn fit_power_law(&self, data: &[T]) -> Result<CurveFittingModel<T>> {
let mut params = Array1::zeros(3);
let n = data.len();
let y_start = data[0];
let y_end = *data.last().expect("unwrap failed");
params[0] = y_start; params[1] = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::zero()); params[2] = y_end - y_start;
Ok(CurveFittingModel {
model_type: CurveFittingMethod::PowerLaw,
parameters: params,
r_squared: scirs2_core::numeric::NumCast::from(0.7).unwrap_or_else(|| T::zero()), confidence: scirs2_core::numeric::NumCast::from(0.7).unwrap_or_else(|| T::zero()),
complexity: 3,
})
}
fn fit_logarithmic(&self, data: &[T]) -> Result<CurveFittingModel<T>> {
let mut params = Array1::zeros(2);
let y_start = data[0];
let y_end = *data.last().expect("unwrap failed");
let n = T::from(data.len() as f64).expect("unwrap failed");
params[0] = (y_end - y_start) / n.ln(); params[1] = y_start;
Ok(CurveFittingModel {
model_type: CurveFittingMethod::Logarithmic,
parameters: params,
r_squared: scirs2_core::numeric::NumCast::from(0.6).unwrap_or_else(|| T::zero()),
confidence: scirs2_core::numeric::NumCast::from(0.6).unwrap_or_else(|| T::zero()),
complexity: 2,
})
}
fn fit_polynomial(&self, data: &[T]) -> Result<CurveFittingModel<T>> {
let mut params = Array1::zeros(3);
let n = data.len();
let y_start = data[0];
let y_end = *data.last().expect("unwrap failed");
let y_mid = if n > 1 { data[n/2] } else { y_start };
params[2] = y_start; params[1] = (y_mid - y_start) / T::from((n/2) as f64).expect("unwrap failed"); params[0] = (y_end - y_start - params[1] * scirs2_core::numeric::NumCast::from(n as f64).unwrap_or_else(|| T::zero())) /
(scirs2_core::numeric::NumCast::from(n as f64).unwrap_or_else(|| T::zero()) * scirs2_core::numeric::NumCast::from(n as f64).unwrap_or_else(|| T::zero()));
Ok(CurveFittingModel {
model_type: CurveFittingMethod::Polynomial,
parameters: params,
r_squared: scirs2_core::numeric::NumCast::from(0.8).unwrap_or_else(|| T::zero()),
confidence: scirs2_core::numeric::NumCast::from(0.8).unwrap_or_else(|| T::zero()),
complexity: 3,
})
}
fn fit_learning_curve(&self, data: &[T]) -> Result<CurveFittingModel<T>> {
let mut params = Array1::zeros(2);
let y_start = data[0];
let y_end = *data.last().expect("unwrap failed");
params[0] = y_end; params[1] = scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero());
Ok(CurveFittingModel {
model_type: CurveFittingMethod::LearningCurve,
parameters: params,
r_squared: scirs2_core::numeric::NumCast::from(0.75).unwrap_or_else(|| T::zero()),
confidence: scirs2_core::numeric::NumCast::from(0.75).unwrap_or_else(|| T::zero()),
complexity: 2,
})
}
fn fit_neural_predictor(&self, data: &[T]) -> Result<CurveFittingModel<T>> {
let input_size = 5; let output_size = 1; let mut params = Array1::zeros(input_size + output_size);
for i in 0..params.len() {
params[i] = T::from(scirs2_core::random::random::<f64>() * 0.1).expect("unwrap failed");
}
Ok(CurveFittingModel {
model_type: CurveFittingMethod::NeuralPredictor,
parameters: params,
r_squared: scirs2_core::numeric::NumCast::from(0.85).unwrap_or_else(|| T::zero()),
confidence: scirs2_core::numeric::NumCast::from(0.85).unwrap_or_else(|| T::zero()),
complexity: input_size + output_size,
})
}
fn extrapolate_performance(&self, model: &CurveFittingModel<T>, target_epoch: usize) -> Result<T> {
let x = scirs2_core::numeric::NumCast::from(target_epoch as f64).unwrap_or_else(|| T::zero());
match model.model_type {
CurveFittingMethod::Exponential => {
Ok(model.parameters[0] * (model.parameters[1] * x).exp() + model.parameters[2])
}
CurveFittingMethod::PowerLaw => {
Ok(model.parameters[0] * x.powf(model.parameters[1]) + model.parameters[2])
}
CurveFittingMethod::Logarithmic => {
Ok(model.parameters[0] * x.ln() + model.parameters[1])
}
CurveFittingMethod::Polynomial => {
Ok(model.parameters[0] * x * x + model.parameters[1] * x + model.parameters[2])
}
CurveFittingMethod::LearningCurve => {
Ok(model.parameters[0] * (T::one() - (-model.parameters[1] * x).exp()))
}
CurveFittingMethod::NeuralPredictor => {
Ok(model.parameters.sum() / T::from(model.parameters.len() as f64).expect("unwrap failed"))
}
}
}
fn has_converged(&self, data: &[T]) -> Result<bool> {
if data.len() < self.config.patience {
return Ok(false);
}
let recent_data = &data[data.len().saturating_sub(self.config.patience)..];
let mean = recent_data.iter().cloned().fold(T::zero(), |acc, x| acc + x) /
T::from(recent_data.len() as f64).expect("unwrap failed");
let variance = recent_data.iter()
.map(|&x| (x - mean) * (x - mean))
.fold(T::zero(), |acc, x| acc + x) / T::from(recent_data.len() as f64).expect("unwrap failed");
let std_dev = variance.sqrt();
Ok(std_dev < self.config.early_stop_threshold)
}
fn has_plateaued(&self, data: &[T]) -> Result<bool> {
if data.len() < self.config.patience * 2 {
return Ok(false);
}
let window_size = self.config.patience;
let recent_data = &data[data.len().saturating_sub(window_size)..];
let earlier_data = &data[data.len().saturating_sub(window_size * 2)..data.len().saturating_sub(window_size)];
let recent_mean = recent_data.iter().cloned().fold(T::zero(), |acc, x| acc + x) /
T::from(recent_data.len() as f64).expect("unwrap failed");
let earlier_mean = earlier_data.iter().cloned().fold(T::zero(), |acc, x| acc + x) /
T::from(earlier_data.len() as f64).expect("unwrap failed");
let improvement = recent_mean - earlier_mean;
Ok(improvement.abs() < self.config.early_stop_threshold)
}
fn is_declining(&self, data: &[T]) -> Result<bool> {
if data.len() < 3 {
return Ok(false);
}
let recent = data.len().saturating_sub(3);
let slope = (data[data.len()-1] - data[recent]) / scirs2_core::numeric::NumCast::from(3.0).unwrap_or_else(|| T::zero());
Ok(slope < -self.config.early_stop_threshold)
}
fn estimate_remaining_time(&self, history: &PerformanceHistory<T>) -> Result<T> {
if history.epoch_times.is_empty() {
return Ok(T::zero());
}
let avg_epoch_time = history.epoch_times.iter().cloned().fold(T::zero(), |acc, t| acc + t) /
T::from(history.epoch_times.len() as f64).expect("unwrap failed");
let remaining_epochs = self.config.max_prediction_epochs.saturating_sub(history.epochs.len());
Ok(avg_epoch_time * scirs2_core::numeric::NumCast::from(remaining_epochs as f64).unwrap_or_else(|| T::zero()))
}
pub fn get_prediction_stats(&self) -> &PredictionStats<T> {
&self.prediction_stats
}
pub fn get_performance_history(&self, arch_id: &str) -> Option<&PerformanceHistory<T>> {
self.performance_history.get(arch_id)
}
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> PredictionStats<T> {
fn new() -> Self {
Self {
mean_absolute_error: T::zero(),
rmse: T::zero(),
correlation: T::zero(),
successful_predictions: 0,
failed_predictions: 0,
early_stop_accuracy: T::zero(),
}
}
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> Default for PerformanceEstimatorConfig<T> {
fn default() -> Self {
Self {
min_epochs: 5,
early_stop_threshold: scirs2_core::numeric::NumCast::from(0.001).unwrap_or_else(|| T::zero()),
patience: 5,
fitting_method: CurveFittingMethod::LearningCurve,
confidence_threshold: scirs2_core::numeric::NumCast::from(0.7).unwrap_or_else(|| T::zero()),
max_prediction_epochs: 100,
smoothing_factor: scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero()),
}
}
}