optirs_nas/multi_objective/
weighted_sum.rs1use crate::error::{OptimError, Result};
4use crate::nas_engine::{
5 MultiObjectiveConfig, ObjectiveConfig, ObjectiveType, OptimizationDirection,
6 OptimizerArchitecture, SearchResult,
7};
8use crate::EvaluationMetric;
9use scirs2_core::numeric::Float;
10use std::cmp::Ordering;
11use std::fmt::Debug;
12
13use super::core::{
14 CreationMethod, Individual, MultiObjectiveOptimizer, MultiObjectiveStatistics, ObjectiveBounds,
15 ParetoFront, ParetoSolution, SolutionMetadata,
16};
17
18pub struct WeightedSum<T: Float + Debug + Send + Sync + 'static> {
20 pub(super) weights: Vec<T>,
22 pub(super) objectives: Vec<ObjectiveConfig<T>>,
25 pub(super) best_solution: Option<Individual<T>>,
27 pub(super) statistics: MultiObjectiveStatistics<T>,
29 pub(super) pareto_front: ParetoFront<T>,
31 pub(super) generation: usize,
33}
34impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> WeightedSum<T> {
35 pub fn new(objectives: &[ObjectiveConfig<T>]) -> Result<Self> {
36 let weights = objectives.iter().map(|obj| obj.weight).collect();
37 Ok(Self {
38 weights,
39 objectives: objectives.to_vec(),
40 best_solution: None,
41 statistics: MultiObjectiveStatistics::default(),
42 pareto_front: ParetoFront::default(),
43 generation: 0,
44 })
45 }
46 pub fn best_solution(&self) -> Option<&Individual<T>> {
50 self.best_solution.as_ref()
51 }
52 pub(super) fn extract_objectives(&self, result: &SearchResult<T>) -> Vec<T> {
57 let mut objectives = Vec::with_capacity(self.objectives.len());
58 for obj_config in &self.objectives {
59 let metric = match obj_config.objective_type {
60 ObjectiveType::Accuracy => EvaluationMetric::Accuracy,
61 ObjectiveType::Loss => EvaluationMetric::FinalPerformance,
62 ObjectiveType::TrainingTime => EvaluationMetric::TrainingTime,
63 ObjectiveType::InferenceTime => EvaluationMetric::ComputationTime,
64 ObjectiveType::MemoryUsage => EvaluationMetric::MemoryUsage,
65 ObjectiveType::EnergyConsumption => EvaluationMetric::ComputationTime,
66 ObjectiveType::ModelSize => EvaluationMetric::MemoryUsage,
67 ObjectiveType::Performance => EvaluationMetric::FinalPerformance,
68 ObjectiveType::Efficiency => EvaluationMetric::ComputationalEfficiency,
69 ObjectiveType::Robustness => EvaluationMetric::Robustness,
70 ObjectiveType::Interpretability => EvaluationMetric::FinalPerformance,
71 ObjectiveType::Fairness => EvaluationMetric::FinalPerformance,
72 ObjectiveType::Privacy => EvaluationMetric::FinalPerformance,
73 ObjectiveType::Sustainability => EvaluationMetric::ComputationalEfficiency,
74 ObjectiveType::Cost => EvaluationMetric::ComputationalEfficiency,
75 ObjectiveType::Custom(_) => EvaluationMetric::FinalPerformance,
76 };
77 let value = result
78 .evaluation_results
79 .metric_scores
80 .get(&metric)
81 .cloned()
82 .unwrap_or(T::zero());
83 objectives.push(value);
84 }
85 objectives
86 }
87 pub fn scalarized_cost_for_result(&self, result: &SearchResult<T>) -> T {
91 let objectives = self.extract_objectives(result);
92 self.scalarize(&objectives, &self.normalized_weights())
93 }
94 pub fn mean_objective_distance(&self, results: &[SearchResult<T>]) -> f64 {
97 if results.len() < 2 {
98 return 0.0;
99 }
100 let vectors: Vec<Vec<T>> = results
101 .iter()
102 .map(|result| self.extract_objectives(result))
103 .collect();
104 let mut total = 0.0;
105 let mut count = 0usize;
106 for i in 0..vectors.len() {
107 for j in (i + 1)..vectors.len() {
108 let a = &vectors[i];
109 let b = &vectors[j];
110 let mut sq_sum = T::zero();
111 for d in 0..a.len().min(b.len()) {
112 let diff = a[d] - b[d];
113 sq_sum = sq_sum + diff * diff;
114 }
115 total += sq_sum.sqrt().to_f64().unwrap_or(0.0);
116 count += 1;
117 }
118 }
119 if count > 0 {
120 total / count as f64
121 } else {
122 0.0
123 }
124 }
125 pub(super) fn dominates(&self, a: &[T], b: &[T]) -> bool {
131 let mut a_strictly_better = false;
132 for (k, obj_config) in self.objectives.iter().enumerate() {
133 if k >= a.len() || k >= b.len() {
134 break;
135 }
136 let val_a = a[k];
137 let val_b = b[k];
138 match obj_config.direction {
139 OptimizationDirection::Minimize => {
140 if val_a > val_b {
141 return false;
142 } else if val_a < val_b {
143 a_strictly_better = true;
144 }
145 }
146 OptimizationDirection::Maximize => {
147 if val_a < val_b {
148 return false;
149 } else if val_a > val_b {
150 a_strictly_better = true;
151 }
152 }
153 }
154 }
155 a_strictly_better
156 }
157 pub(super) fn scalarize(&self, objectives: &[T], normalized_weights: &[T]) -> T {
161 let mut score = T::zero();
162 for (k, &weight) in normalized_weights.iter().enumerate() {
163 if k >= objectives.len() {
164 break;
165 }
166 let directed = match self.objectives.get(k).map(|c| &c.direction) {
167 Some(OptimizationDirection::Maximize) => -objectives[k],
168 _ => objectives[k],
169 };
170 score = score + weight * directed;
171 }
172 score
173 }
174 pub(super) fn normalized_weights(&self) -> Vec<T> {
177 let n = self.weights.len();
178 if n == 0 {
179 return Vec::new();
180 }
181 let mut sum = T::zero();
182 for &w in &self.weights {
183 if w > T::zero() {
184 sum = sum + w;
185 }
186 }
187 if sum > T::zero() {
188 self.weights
189 .iter()
190 .map(|&w| if w > T::zero() { w / sum } else { T::zero() })
191 .collect()
192 } else {
193 let uniform = T::one() / T::from(n).unwrap_or_else(T::one);
194 vec![uniform; n]
195 }
196 }
197 pub(super) fn refresh_front_metrics(&mut self) {
201 if self.pareto_front.solutions.is_empty() {
202 self.pareto_front.objective_bounds = ObjectiveBounds {
203 min_values: Vec::new(),
204 max_values: Vec::new(),
205 ideal_point: Vec::new(),
206 nadir_point: Vec::new(),
207 };
208 self.pareto_front.metrics.num_solutions = 0;
209 self.statistics.pareto_front_size = 0;
210 return;
211 }
212 let num_objectives = self.pareto_front.solutions[0].objectives.len();
213 let mut min_values = vec![T::infinity(); num_objectives];
214 let mut max_values = vec![T::neg_infinity(); num_objectives];
215 for solution in &self.pareto_front.solutions {
216 for (i, &obj_val) in solution.objectives.iter().enumerate() {
217 if i >= num_objectives {
218 break;
219 }
220 if obj_val < min_values[i] {
221 min_values[i] = obj_val;
222 }
223 if obj_val > max_values[i] {
224 max_values[i] = obj_val;
225 }
226 }
227 }
228 self.pareto_front.objective_bounds = ObjectiveBounds {
229 min_values: min_values.clone(),
230 max_values: max_values.clone(),
231 ideal_point: min_values,
232 nadir_point: max_values,
233 };
234 let count = self.pareto_front.solutions.len();
235 self.pareto_front.metrics.num_solutions = count;
236 self.statistics.pareto_front_size = count;
237 }
238}
239impl<
240 T: Float
241 + Debug
242 + Default
243 + Clone
244 + Send
245 + Sync
246 + std::fmt::Debug
247 + PartialOrd
248 + std::iter::Sum,
249 > MultiObjectiveOptimizer<T> for WeightedSum<T>
250{
251 fn initialize(&mut self, config: &MultiObjectiveConfig<T>) -> Result<()> {
262 if config.objectives.is_empty() {
263 return Err(OptimError::InvalidConfig(
264 "WeightedSum has nothing to scalarize: the supplied \
265 MultiObjectiveConfig declares no objectives"
266 .to_string(),
267 ));
268 }
269 self.weights = config.objectives.iter().map(|obj| obj.weight).collect();
270 self.objectives = config.objectives.clone();
271 self.best_solution = None;
272 self.generation = 0;
273 self.statistics = MultiObjectiveStatistics::default();
274 self.pareto_front = ParetoFront::default();
275 Ok(())
276 }
277 fn update_pareto_front(&mut self, new_solutions: &[SearchResult<T>]) -> Result<ParetoFront<T>> {
278 if new_solutions.is_empty() {
279 return Ok(self.pareto_front.clone());
280 }
281 self.generation += 1;
282 self.statistics.total_evaluations += new_solutions.len();
283 let mut candidates: Vec<(Vec<T>, ParetoSolution<T>)> =
284 Vec::with_capacity(self.pareto_front.solutions.len() + new_solutions.len());
285 for solution in &self.pareto_front.solutions {
286 candidates.push((solution.objectives.clone(), solution.clone()));
287 }
288 for result in new_solutions {
289 let objectives = self.extract_objectives(result);
290 let solution = ParetoSolution {
291 architecture: result.architecture.clone(),
292 objectives: objectives.clone(),
293 constraint_violations: Vec::new(),
294 rank: 0,
295 crowding_distance: T::zero(),
296 metadata: SolutionMetadata {
297 id: result.architecture.architecture_id.clone(),
298 generation: self.generation,
299 evaluation_count: self.statistics.total_evaluations,
300 parents: Vec::new(),
301 creation_method: CreationMethod::RandomGeneration,
302 },
303 };
304 candidates.push((objectives, solution));
305 }
306 let mut front: Vec<ParetoSolution<T>> = Vec::new();
307 for i in 0..candidates.len() {
308 let mut dominated = false;
309 for j in 0..candidates.len() {
310 if i != j && self.dominates(&candidates[j].0, &candidates[i].0) {
311 dominated = true;
312 break;
313 }
314 }
315 if dominated {
316 continue;
317 }
318 let is_duplicate = front
319 .iter()
320 .any(|existing| existing.objectives == candidates[i].0);
321 if !is_duplicate {
322 front.push(candidates[i].1.clone());
323 }
324 }
325 self.pareto_front.solutions = front;
326 self.pareto_front.generation = self.generation;
327 self.pareto_front.last_updated = std::time::SystemTime::now();
328 self.refresh_front_metrics();
329 Ok(self.pareto_front.clone())
330 }
331 fn get_pareto_front(&self) -> &ParetoFront<T> {
332 &self.pareto_front
333 }
334 fn select_candidates(
335 &mut self,
336 population: &[OptimizerArchitecture<T>],
337 objectives: &[T],
338 ) -> Result<Vec<OptimizerArchitecture<T>>> {
339 if population.is_empty() {
340 return Ok(Vec::new());
341 }
342 let num_objectives = self.objectives.len();
343 let normalized_weights = self.normalized_weights();
344 let mut scored: Vec<(usize, T)> = Vec::with_capacity(population.len());
345 for (idx, _architecture) in population.iter().enumerate() {
346 let obj_vector: Vec<T> = if num_objectives > 0 {
347 (0..num_objectives)
348 .map(|k| {
349 let flat = idx * num_objectives + k;
350 objectives.get(flat).cloned().unwrap_or(T::zero())
351 })
352 .collect()
353 } else {
354 Vec::new()
355 };
356 let score = self.scalarize(&obj_vector, &normalized_weights);
357 scored.push((idx, score));
358 }
359 scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
360 if let Some(&(best_idx, best_score)) = scored.first() {
361 let best_objectives: Vec<T> = if num_objectives > 0 {
362 (0..num_objectives)
363 .map(|k| {
364 let flat = best_idx * num_objectives + k;
365 objectives.get(flat).cloned().unwrap_or(T::zero())
366 })
367 .collect()
368 } else {
369 Vec::new()
370 };
371 self.best_solution = Some(Individual {
372 architecture: population[best_idx].clone(),
373 objectives: best_objectives,
374 constraints: Vec::new(),
375 rank: 0,
376 crowding_distance: T::zero(),
377 fitness: best_score,
378 id: population[best_idx].architecture_id.clone(),
379 });
380 }
381 let selection_count = (population.len() / 2).max(1).min(population.len());
382 let selected = scored
383 .iter()
384 .take(selection_count)
385 .map(|&(idx, _)| population[idx].clone())
386 .collect();
387 Ok(selected)
388 }
389 fn name(&self) -> &str {
390 "WeightedSum"
391 }
392 fn get_statistics(&self) -> MultiObjectiveStatistics<T> {
393 self.statistics.clone()
394 }
395}