use std::fmt::Debug;
#[allow(dead_code)]
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::numeric::Float;
use std::collections::{HashMap, HashSet};
use crate::error::{OptimError, Result};
#[derive(Debug, Clone)]
pub struct MultiObjectiveConfig<T: Float + Debug + Send + Sync + 'static> {
pub objectives: Vec<Objective<T>>,
pub pareto_method: ParetoMethod,
pub scalarization_method: ScalarizationMethod<T>,
pub num_reference_points: usize,
pub population_size: usize,
pub normalization_method: NormalizationMethod,
pub constraint_handling: ConstraintHandling<T>,
}
#[derive(Debug, Clone)]
pub struct Objective<T: Float + Debug + Send + Sync + 'static> {
pub id: String,
pub name: String,
pub direction: ObjectiveDirection,
pub weight: T,
pub priority: usize,
pub range: Option<(T, T)>,
pub constraint_type: ConstraintType<T>,
pub evaluator: ObjectiveEvaluator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectiveDirection {
Maximize,
Minimize,
}
#[derive(Debug, Clone)]
pub enum ConstraintType<T: Float + Debug + Send + Sync + 'static> {
None,
Hard { threshold: T },
Soft { threshold: T, penalty: T },
Range { min: T, max: T },
}
#[derive(Debug, Clone)]
pub enum ObjectiveEvaluator {
Accuracy,
Latency,
Memory,
Energy,
ModelSize,
FLOPS,
Custom(String),
}
#[derive(Debug, Clone, Copy)]
pub enum ParetoMethod {
NSGA2,
SPEA2,
MOEAD,
IBEA,
NSGA3,
}
#[derive(Debug, Clone)]
pub enum ScalarizationMethod<T: Float + Debug + Send + Sync + 'static> {
WeightedSum,
WeightedProduct,
Tchebycheff { reference_point: Array1<T> },
Achievement { reference_point: Array1<T>, weights: Array1<T> },
PBI { reference_point: Array1<T>, penalty_parameter: T },
}
#[derive(Debug, Clone, Copy)]
pub enum NormalizationMethod {
MinMax,
ZScore,
Robust,
None,
}
#[derive(Debug, Clone)]
pub enum ConstraintHandling<T: Float + Debug + Send + Sync + 'static> {
Penalty { penalty_factor: T },
FeasibilityBased,
ConstraintDomination,
EpsilonConstrained { epsilon: T },
}
#[derive(Debug)]
pub struct MultiObjectiveEvaluator<T: Float + Debug + Send + Sync + 'static> {
config: MultiObjectiveConfig<T>,
pareto_front: ParetoFront<T>,
archive: Archive<T>,
objective_stats: HashMap<String, ObjectiveStats<T>>,
evaluation_history: Vec<MultiObjectiveEvaluation<T>>,
reference_points: Array2<T>,
scalarization_functions: Vec<ScalarizationFunction<T>>,
}
#[derive(Debug, Clone)]
pub struct ParetoFront<T: Float + Debug + Send + Sync + 'static> {
pub solutions: Vec<MultiObjectiveSolution<T>>,
pub hypervolume: T,
pub diversity: DiversityMetrics<T>,
pub convergence: ConvergenceMetrics<T>,
}
#[derive(Debug, Clone)]
pub struct MultiObjectiveSolution<T: Float + Debug + Send + Sync + 'static> {
pub id: String,
pub architecture: String,
pub objective_values: HashMap<String, T>,
pub normalized_values: HashMap<String, T>,
pub constraint_violations: HashMap<String, T>,
pub dominance_rank: usize,
pub crowding_distance: T,
pub is_feasible: bool,
}
#[derive(Debug)]
pub struct Archive<T: Float + Debug + Send + Sync + 'static> {
pub max_size: usize,
pub solutions: Vec<MultiObjectiveSolution<T>>,
pub management_strategy: ArchiveManagementStrategy,
}
#[derive(Debug, Clone, Copy)]
pub enum ArchiveManagementStrategy {
FixedSize,
Adaptive,
CrowdingBased,
}
#[derive(Debug, Clone)]
pub struct ObjectiveStats<T: Float + Debug + Send + Sync + 'static> {
pub min_value: T,
pub max_value: T,
pub mean_value: T,
pub std_deviation: T,
pub median_value: T,
pub num_evaluations: usize,
}
#[derive(Debug, Clone)]
pub struct MultiObjectiveEvaluation<T: Float + Debug + Send + Sync + 'static> {
pub solution: MultiObjectiveSolution<T>,
pub scalarized_fitness: T,
pub dominance_info: DominanceInfo<T>,
pub timestamp: usize,
}
#[derive(Debug, Clone)]
pub struct DominanceInfo<T: Float + Debug + Send + Sync + 'static> {
pub dominates_count: usize,
pub dominated_by_count: usize,
pub dominated_solutions: HashSet<String>,
pub dominating_solutions: HashSet<String>,
pub non_domination_rank: usize,
}
#[derive(Debug, Clone)]
pub struct DiversityMetrics<T: Float + Debug + Send + Sync + 'static> {
pub spacing: T,
pub spread: T,
pub max_spread: T,
pub diversity_index: T,
}
#[derive(Debug, Clone)]
pub struct ConvergenceMetrics<T: Float + Debug + Send + Sync + 'static> {
pub generational_distance: T,
pub inverted_generational_distance: T,
pub epsilon_indicator: T,
pub r2_indicator: T,
}
#[derive(Debug, Clone)]
pub struct ScalarizationFunction<T: Float + Debug + Send + Sync + 'static> {
pub id: String,
pub method: ScalarizationMethod<T>,
pub reference_direction: Array1<T>,
pub weight_vector: Array1<T>,
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> MultiObjectiveEvaluator<T> {
pub fn new(config: MultiObjectiveConfig<T>) -> Result<Self> {
let num_objectives = config.objectives.len();
let reference_points = Self::generate_reference_points(num_objectives, config.num_reference_points)?;
let scalarization_functions = Self::create_scalarization_functions(&config, &reference_points)?;
Ok(Self {
config,
pareto_front: ParetoFront::new(),
archive: Archive::new(1000), objective_stats: HashMap::new(),
evaluation_history: Vec::new(),
reference_points,
scalarization_functions,
})
}
pub fn evaluate(&mut self, arch_id: &str, architecture_data: &HashMap<String, f64>) -> Result<MultiObjectiveEvaluation<T>> {
let mut objective_values = HashMap::new();
let mut constraint_violations = HashMap::new();
for objective in &self.config.objectives {
let value = self.evaluate_single_objective(objective, architecture_data)?;
objective_values.insert(objective.id.clone(), value);
let violation = self.check_constraint(objective, value)?;
if violation > T::zero() {
constraint_violations.insert(objective.id.clone(), violation);
}
self.update_objective_stats(&objective.id, value)?;
}
let normalized_values = self.normalize_objectives(&objective_values)?;
let solution = MultiObjectiveSolution {
id: arch_id.to_string(),
architecture: arch_id.to_string(),
objective_values: objective_values.clone(),
normalized_values,
constraint_violations: constraint_violations.clone(),
dominance_rank: 0,
crowding_distance: T::zero(),
is_feasible: constraint_violations.is_empty(),
};
let scalarized_fitness = self.compute_scalarized_fitness(&solution)?;
let dominance_info = self.compute_dominance_info(&solution)?;
let evaluation = MultiObjectiveEvaluation {
solution: solution.clone(),
scalarized_fitness,
dominance_info,
timestamp: self.evaluation_history.len(),
};
self.update_archive(solution)?;
self.update_pareto_front()?;
self.evaluation_history.push(evaluation.clone());
Ok(evaluation)
}
fn evaluate_single_objective(&self, objective: &Objective<T>, data: &HashMap<String, f64>) -> Result<T> {
match &objective.evaluator {
ObjectiveEvaluator::Accuracy => {
let acc = data.get("accuracy").unwrap_or(&0.0);
Ok(scirs2_core::numeric::NumCast::from(*acc).unwrap_or_else(|| T::zero()))
}
ObjectiveEvaluator::Latency => {
let latency = data.get("latency_ms").unwrap_or(&100.0);
Ok(scirs2_core::numeric::NumCast::from(*latency).unwrap_or_else(|| T::zero()))
}
ObjectiveEvaluator::Memory => {
let memory = data.get("memory_mb").unwrap_or(&512.0);
Ok(scirs2_core::numeric::NumCast::from(*memory).unwrap_or_else(|| T::zero()))
}
ObjectiveEvaluator::Energy => {
let energy = data.get("energy_j").unwrap_or(&10.0);
Ok(scirs2_core::numeric::NumCast::from(*energy).unwrap_or_else(|| T::zero()))
}
ObjectiveEvaluator::ModelSize => {
let size = data.get("model_size_mb").unwrap_or(&50.0);
Ok(scirs2_core::numeric::NumCast::from(*size).unwrap_or_else(|| T::zero()))
}
ObjectiveEvaluator::FLOPS => {
let flops = data.get("flops").unwrap_or(&1e9);
Ok(scirs2_core::numeric::NumCast::from(*flops).unwrap_or_else(|| T::zero()))
}
ObjectiveEvaluator::Custom(name) => {
let value = data.get(name).unwrap_or(&0.0);
Ok(scirs2_core::numeric::NumCast::from(*value).unwrap_or_else(|| T::zero()))
}
}
}
fn check_constraint(&self, objective: &Objective<T>, value: T) -> Result<T> {
match &objective.constraint_type {
ConstraintType::None => Ok(T::zero()),
ConstraintType::Hard { threshold } => {
match objective.direction {
ObjectiveDirection::Maximize => {
if value < *threshold {
Ok(*threshold - value)
} else {
Ok(T::zero())
}
}
ObjectiveDirection::Minimize => {
if value > *threshold {
Ok(value - *threshold)
} else {
Ok(T::zero())
}
}
}
}
ConstraintType::Soft { threshold, penalty: _ } => {
self.check_constraint(&Objective {
constraint_type: ConstraintType::Hard { threshold: *threshold },
..objective.clone()
}, value)
}
ConstraintType::Range { min, max } => {
if value < *min {
Ok(*min - value)
} else if value > *max {
Ok(value - *max)
} else {
Ok(T::zero())
}
}
}
}
fn normalize_objectives(&self, values: &HashMap<String, T>) -> Result<HashMap<String, T>> {
let mut normalized = HashMap::new();
match self.config.normalization_method {
NormalizationMethod::MinMax => {
for (obj_id, &value) in values {
if let Some(stats) = self.objective_stats.get(obj_id) {
let range = stats.max_value - stats.min_value;
let normalized_value = if range > T::zero() {
(value - stats.min_value) / range
} else {
T::zero()
};
normalized.insert(obj_id.clone(), normalized_value);
} else {
normalized.insert(obj_id.clone(), value);
}
}
}
NormalizationMethod::ZScore => {
for (obj_id, &value) in values {
if let Some(stats) = self.objective_stats.get(obj_id) {
let normalized_value = if stats.std_deviation > T::zero() {
(value - stats.mean_value) / stats.std_deviation
} else {
T::zero()
};
normalized.insert(obj_id.clone(), normalized_value);
} else {
normalized.insert(obj_id.clone(), value);
}
}
}
_ => {
normalized = values.clone();
}
}
Ok(normalized)
}
fn compute_scalarized_fitness(&self, solution: &MultiObjectiveSolution<T>) -> Result<T> {
match &self.config.scalarization_method {
ScalarizationMethod::WeightedSum => {
let mut fitness = T::zero();
for objective in &self.config.objectives {
if let Some(&value) = solution.normalized_values.get(&objective.id) {
let contribution = match objective.direction {
ObjectiveDirection::Maximize => objective.weight * value,
ObjectiveDirection::Minimize => objective.weight * (-value),
};
fitness = fitness + contribution;
}
}
Ok(fitness)
}
ScalarizationMethod::WeightedProduct => {
let mut fitness = T::one();
for objective in &self.config.objectives {
if let Some(&value) = solution.normalized_values.get(&objective.id) {
let contribution = match objective.direction {
ObjectiveDirection::Maximize => value.powf(objective.weight),
ObjectiveDirection::Minimize => (T::one() - value).powf(objective.weight),
};
fitness = fitness * contribution;
}
}
Ok(fitness)
}
ScalarizationMethod::Tchebycheff { reference_point } => {
let mut max_diff = T::zero();
for (i, objective) in self.config.objectives.iter().enumerate() {
if let Some(&value) = solution.normalized_values.get(&objective.id) {
if i < reference_point.len() {
let diff = match objective.direction {
ObjectiveDirection::Maximize => objective.weight * (reference_point[i] - value).abs(),
ObjectiveDirection::Minimize => objective.weight * (value - reference_point[i]).abs(),
};
max_diff = max_diff.max(diff);
}
}
}
Ok(-max_diff) }
_ => {
self.compute_scalarized_fitness(&MultiObjectiveSolution {
config: MultiObjectiveConfig {
scalarization_method: ScalarizationMethod::WeightedSum,
..self.config.clone()
},
..solution.clone()
})
}
}
}
fn compute_dominance_info(&self, solution: &MultiObjectiveSolution<T>) -> Result<DominanceInfo<T>> {
let mut dominates_count = 0;
let mut dominated_by_count = 0;
let mut dominated_solutions = HashSet::new();
let mut dominating_solutions = HashSet::new();
for archived_solution in &self.archive.solutions {
if solution.id != archived_solution.id {
match self.compare_dominance(solution, archived_solution)? {
DominanceRelation::Dominates => {
dominates_count += 1;
dominated_solutions.insert(archived_solution.id.clone());
}
DominanceRelation::DominatedBy => {
dominated_by_count += 1;
dominating_solutions.insert(archived_solution.id.clone());
}
DominanceRelation::NonDominated => {
}
}
}
}
Ok(DominanceInfo {
dominates_count,
dominated_by_count,
dominated_solutions,
dominating_solutions,
non_domination_rank: 0, })
}
fn compare_dominance(&self, solution1: &MultiObjectiveSolution<T>, solution2: &MultiObjectiveSolution<T>) -> Result<DominanceRelation> {
let mut solution1_better = false;
let mut solution2_better = false;
for objective in &self.config.objectives {
let val1 = solution1.objective_values.get(&objective.id).unwrap_or(&T::zero());
let val2 = solution2.objective_values.get(&objective.id).unwrap_or(&T::zero());
match objective.direction {
ObjectiveDirection::Maximize => {
if *val1 > *val2 {
solution1_better = true;
} else if *val2 > *val1 {
solution2_better = true;
}
}
ObjectiveDirection::Minimize => {
if *val1 < *val2 {
solution1_better = true;
} else if *val2 < *val1 {
solution2_better = true;
}
}
}
}
if solution1_better && !solution2_better {
Ok(DominanceRelation::Dominates)
} else if solution2_better && !solution1_better {
Ok(DominanceRelation::DominatedBy)
} else {
Ok(DominanceRelation::NonDominated)
}
}
fn update_objective_stats(&mut self, obj_id: &str, value: T) -> Result<()> {
let stats = self.objective_stats.entry(obj_id.to_string()).or_insert_with(|| ObjectiveStats {
min_value: value,
max_value: value,
mean_value: value,
std_deviation: T::zero(),
median_value: value,
num_evaluations: 0,
});
stats.min_value = stats.min_value.min(value);
stats.max_value = stats.max_value.max(value);
stats.num_evaluations += 1;
let n = scirs2_core::numeric::NumCast::from(stats.num_evaluations as f64).unwrap_or_else(|| T::zero());
stats.mean_value = (stats.mean_value * (n - T::one()) + value) / n;
Ok(())
}
fn update_archive(&mut self, solution: MultiObjectiveSolution<T>) -> Result<()> {
self.archive.solutions.push(solution);
self.remove_dominated_solutions()?;
if self.archive.solutions.len() > self.archive.max_size {
self.reduce_archive_size()?;
}
Ok(())
}
fn remove_dominated_solutions(&mut self) -> Result<()> {
let mut non_dominated = Vec::new();
for i in 0..self.archive.solutions.len() {
let mut is_dominated = false;
for j in 0..self.archive.solutions.len() {
if i != j {
if let Ok(DominanceRelation::DominatedBy) = self.compare_dominance(&self.archive.solutions[i], &self.archive.solutions[j]) {
is_dominated = true;
break;
}
}
}
if !is_dominated {
non_dominated.push(self.archive.solutions[i].clone());
}
}
self.archive.solutions = non_dominated;
Ok(())
}
fn reduce_archive_size(&mut self) -> Result<()> {
self.compute_crowding_distances()?;
self.archive.solutions.sort_by(|a, b| b.crowding_distance.partial_cmp(&a.crowding_distance).unwrap_or(std::cmp::Ordering::Equal));
self.archive.solutions.truncate(self.archive.max_size);
Ok(())
}
fn compute_crowding_distances(&mut self) -> Result<()> {
let n = self.archive.solutions.len();
if n <= 2 {
return Ok(());
}
for solution in &mut self.archive.solutions {
solution.crowding_distance = T::zero();
}
for objective in &self.config.objectives {
self.archive.solutions.sort_by(|a, b| {
let val_a = a.objective_values.get(&objective.id).unwrap_or(&T::zero());
let val_b = b.objective_values.get(&objective.id).unwrap_or(&T::zero());
val_a.partial_cmp(val_b).unwrap_or(std::cmp::Ordering::Equal)
});
self.archive.solutions[0].crowding_distance = scirs2_core::numeric::NumCast::from(f64::INFINITY).unwrap_or_else(|| T::zero());
self.archive.solutions[n-1].crowding_distance = scirs2_core::numeric::NumCast::from(f64::INFINITY).unwrap_or_else(|| T::zero());
let obj_range = *self.archive.solutions[n-1].objective_values.get(&objective.id).unwrap_or(&T::zero()) -
*self.archive.solutions[0].objective_values.get(&objective.id).unwrap_or(&T::zero());
if obj_range > T::zero() {
for i in 1..n-1 {
let prev_val = *self.archive.solutions[i-1].objective_values.get(&objective.id).unwrap_or(&T::zero());
let next_val = *self.archive.solutions[i+1].objective_values.get(&objective.id).unwrap_or(&T::zero());
let distance = (next_val - prev_val) / obj_range;
self.archive.solutions[i].crowding_distance = self.archive.solutions[i].crowding_distance + distance;
}
}
}
Ok(())
}
fn update_pareto_front(&mut self) -> Result<()> {
self.pareto_front.solutions = self.archive.solutions.clone();
self.pareto_front.hypervolume = self.compute_hypervolume()?;
self.pareto_front.diversity = self.compute_diversity_metrics()?;
Ok(())
}
fn compute_hypervolume(&self) -> Result<T> {
if self.archive.solutions.is_empty() {
return Ok(T::zero());
}
let mut volume = T::zero();
for solution in &self.archive.solutions {
let mut product = T::one();
for objective in &self.config.objectives {
if let Some(&value) = solution.normalized_values.get(&objective.id) {
product = product * value.max(T::zero());
}
}
volume = volume + product;
}
Ok(volume)
}
fn compute_diversity_metrics(&self) -> Result<DiversityMetrics<T>> {
if self.archive.solutions.len() < 2 {
return Ok(DiversityMetrics {
spacing: T::zero(),
spread: T::zero(),
max_spread: T::zero(),
diversity_index: T::zero(),
});
}
let spacing = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::zero()); let spread = scirs2_core::numeric::NumCast::from(0.8).unwrap_or_else(|| T::zero()); let max_spread = T::one();
let diversity_index = scirs2_core::numeric::NumCast::from(0.7).unwrap_or_else(|| T::zero());
Ok(DiversityMetrics {
spacing,
spread,
max_spread,
diversity_index,
})
}
fn generate_reference_points(num_objectives: usize, num_points: usize) -> Result<Array2<T>> {
let mut points = Array2::zeros((num_points, num_objectives));
for i in 0..num_points {
for j in 0..num_objectives {
let value = T::from((i + j) as f64 / (num_points + num_objectives) as f64).expect("unwrap failed");
points[[i, j]] = value;
}
let sum: T = points.row(i).sum();
if sum > T::zero() {
for j in 0..num_objectives {
points[[i, j]] = points[[i, j]] / sum;
}
}
}
Ok(points)
}
fn create_scalarization_functions(config: &MultiObjectiveConfig<T>, reference_points: &Array2<T>) -> Result<Vec<ScalarizationFunction<T>>> {
let mut functions = Vec::new();
for i in 0..reference_points.nrows() {
let reference_direction = reference_points.row(i).to_owned();
let weight_vector = reference_direction.clone();
functions.push(ScalarizationFunction {
id: format!("scalarization_{}", i),
method: config.scalarization_method.clone(),
reference_direction,
weight_vector,
});
}
Ok(functions)
}
pub fn get_pareto_front(&self) -> &ParetoFront<T> {
&self.pareto_front
}
pub fn get_archive(&self) -> &Archive<T> {
&self.archive
}
pub fn get_evaluation_history(&self) -> &[MultiObjectiveEvaluation<T>] {
&self.evaluation_history
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DominanceRelation {
Dominates,
DominatedBy,
NonDominated,
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> ParetoFront<T> {
fn new() -> Self {
Self {
solutions: Vec::new(),
hypervolume: T::zero(),
diversity: DiversityMetrics {
spacing: T::zero(),
spread: T::zero(),
max_spread: T::zero(),
diversity_index: T::zero(),
},
convergence: ConvergenceMetrics {
generational_distance: T::zero(),
inverted_generational_distance: T::zero(),
epsilon_indicator: T::zero(),
r2_indicator: T::zero(),
},
}
}
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> Archive<T> {
fn new(max_size: usize) -> Self {
Self {
max_size,
solutions: Vec::new(),
management_strategy: ArchiveManagementStrategy::CrowdingBased,
}
}
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> Default for MultiObjectiveConfig<T> {
fn default() -> Self {
Self {
objectives: vec![
Objective {
id: "accuracy".to_string(),
name: "Validation Accuracy".to_string(),
direction: ObjectiveDirection::Maximize,
weight: scirs2_core::numeric::NumCast::from(0.4).unwrap_or_else(|| T::zero()),
priority: 1,
range: Some((T::zero(), T::one())),
constraint_type: ConstraintType::None,
evaluator: ObjectiveEvaluator::Accuracy,
},
Objective {
id: "latency".to_string(),
name: "Inference Latency".to_string(),
direction: ObjectiveDirection::Minimize,
weight: scirs2_core::numeric::NumCast::from(0.3).unwrap_or_else(|| T::zero()),
priority: 2,
range: Some((T::zero(), scirs2_core::numeric::NumCast::from(1000.0).unwrap_or_else(|| T::zero()))),
constraint_type: ConstraintType::Hard { threshold: scirs2_core::numeric::NumCast::from(100.0).unwrap_or_else(|| T::zero()) },
evaluator: ObjectiveEvaluator::Latency,
},
Objective {
id: "memory".to_string(),
name: "Memory Usage".to_string(),
direction: ObjectiveDirection::Minimize,
weight: scirs2_core::numeric::NumCast::from(0.3).unwrap_or_else(|| T::zero()),
priority: 3,
range: Some((T::zero(), scirs2_core::numeric::NumCast::from(2048.0).unwrap_or_else(|| T::zero()))),
constraint_type: ConstraintType::Soft {
threshold: scirs2_core::numeric::NumCast::from(1024.0).unwrap_or_else(|| T::zero()),
penalty: scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero())
},
evaluator: ObjectiveEvaluator::Memory,
},
],
pareto_method: ParetoMethod::NSGA2,
scalarization_method: ScalarizationMethod::WeightedSum,
num_reference_points: 100,
population_size: 50,
normalization_method: NormalizationMethod::MinMax,
constraint_handling: ConstraintHandling::Penalty { penalty_factor: scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero()) },
}
}
}