1use crate::error::{OptimError, Result};
4use crate::nas_engine::{
5 MultiObjectiveConfig, OptimizationDirection, OptimizerArchitecture, SearchResult,
6};
7use scirs2_core::numeric::Float;
8use scirs2_core::random::Random;
9use std::cmp::Ordering;
10use std::fmt::Debug;
11
12use super::core::{
13 CreationMethod, Individual, MultiObjectiveOptimizer, MultiObjectiveStatistics, ObjectiveBounds,
14 ParetoFront, ParetoSolution, SolutionMetadata,
15};
16use super::hypervolume::{
17 derive_reference_point, hypervolume_minimization, normalize_front_for_minimization,
18 normalize_reference_for_minimization,
19};
20use super::metrics;
21use super::operators;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub(super) enum DominanceRelation {
26 Dominates,
27 DominatedBy,
28 NonDominated,
29}
30pub struct NSGA2<T: Float + Debug + Send + Sync + 'static> {
32 pub(super) config: MultiObjectiveConfig<T>,
34 pub(super) population: Vec<Individual<T>>,
36 pub(super) pareto_front: ParetoFront<T>,
38 pub(super) generation: usize,
40 pub(super) statistics: MultiObjectiveStatistics<T>,
42 pub(super) population_size: usize,
44 pub(super) crossover_prob: f64,
46 pub(super) mutation_prob: f64,
48 pub(super) rng: Random<scirs2_core::random::rngs::StdRng>,
50 pub(super) hypervolume_reference: Option<Vec<T>>,
60 pub(super) previous_hypervolume: Option<T>,
63}
64impl<
66 T: Float
67 + Debug
68 + Default
69 + Clone
70 + Send
71 + Sync
72 + std::fmt::Debug
73 + PartialOrd
74 + std::iter::Sum,
75 > NSGA2<T>
76{
77 pub fn new(population_size: usize, crossover_prob: f64, mutation_prob: f64) -> Self {
85 Self::with_seed(
86 population_size,
87 crossover_prob,
88 mutation_prob,
89 scirs2_core::random::random::<u64>(),
90 )
91 }
92
93 pub fn with_seed(
95 population_size: usize,
96 crossover_prob: f64,
97 mutation_prob: f64,
98 seed: u64,
99 ) -> Self {
100 Self {
101 config: MultiObjectiveConfig::default(),
102 population: Vec::new(),
103 pareto_front: ParetoFront::new(),
104 generation: 0,
105 statistics: MultiObjectiveStatistics::default(),
106 population_size,
107 crossover_prob,
108 mutation_prob,
109 rng: Random::seed(seed),
110 hypervolume_reference: None,
111 previous_hypervolume: None,
112 }
113 }
114
115 pub fn set_hypervolume_reference(&mut self, reference: Vec<T>) -> Result<()> {
119 if reference.len() != self.config.objectives.len() {
120 return Err(OptimError::InvalidConfig(format!(
121 "hypervolume reference has {} entries but {} objectives are configured",
122 reference.len(),
123 self.config.objectives.len()
124 )));
125 }
126 self.hypervolume_reference = Some(reference);
127 Ok(())
128 }
129
130 pub fn hypervolume_reference(&self) -> Option<&[T]> {
133 self.hypervolume_reference.as_deref()
134 }
135
136 pub(super) fn objective_directions(&self) -> Vec<OptimizationDirection> {
138 self.config
139 .objectives
140 .iter()
141 .map(|objective| objective.direction.clone())
142 .collect()
143 }
144
145 pub(super) fn front_in_minimization_space(&self) -> Vec<Vec<T>> {
149 let directions = self.objective_directions();
150 let raw: Vec<Vec<T>> = self
151 .pareto_front
152 .solutions
153 .iter()
154 .map(|solution| solution.objectives.clone())
155 .collect();
156 normalize_front_for_minimization(&raw, &directions)
157 }
158 pub(super) fn initialize_population(&mut self) -> Result<()> {
166 self.population.clear();
167 for i in 0..self.population_size {
168 let architecture = self.generate_random_architecture()?;
169 let individual = Individual {
170 id: format!("ind_{}", i),
171 architecture,
172 objectives: Vec::new(),
173 constraints: Vec::new(),
174 rank: 0,
175 crowding_distance: T::zero(),
176 fitness: T::zero(),
177 };
178 self.population.push(individual);
179 }
180 Ok(())
181 }
182 pub(super) fn generate_random_architecture(&mut self) -> Result<OptimizerArchitecture<T>> {
186 Ok(operators::sample_architecture(&mut self.rng, "arch"))
187 }
188 pub(super) fn is_evaluated(&self, individual: &Individual<T>) -> bool {
193 !self.config.objectives.is_empty()
194 && individual.objectives.len() == self.config.objectives.len()
195 }
196 pub(super) fn non_dominated_sort(&mut self) -> Vec<Vec<usize>> {
198 let n = self.population.len();
199 let mut fronts = Vec::new();
200 let mut domination_count = vec![0; n];
201 let mut dominated_solutions = vec![Vec::new(); n];
202 let mut first_front = Vec::new();
203 for i in 0..n {
204 for j in 0..n {
205 if i != j {
206 let dominance = self.dominance_relation(i, j);
207 match dominance {
208 DominanceRelation::Dominates => {
209 dominated_solutions[i].push(j);
210 }
211 DominanceRelation::DominatedBy => {
212 domination_count[i] += 1;
213 }
214 DominanceRelation::NonDominated => {}
215 }
216 }
217 }
218 if domination_count[i] == 0 {
219 self.population[i].rank = 0;
220 first_front.push(i);
221 }
222 }
223 fronts.push(first_front.clone());
224 let mut current_front = first_front;
225 let mut rank = 0;
226 while !current_front.is_empty() {
227 let mut next_front = Vec::new();
228 for &i in ¤t_front {
229 for &j in &dominated_solutions[i] {
230 domination_count[j] -= 1;
231 if domination_count[j] == 0 {
232 self.population[j].rank = rank + 1;
233 next_front.push(j);
234 }
235 }
236 }
237 rank += 1;
238 current_front = next_front.clone();
239 if !next_front.is_empty() {
240 fronts.push(next_front);
241 }
242 }
243 fronts
244 }
245 pub(super) fn dominance_relation(&self, i: usize, j: usize) -> DominanceRelation {
253 let ind_i = &self.population[i];
254 let ind_j = &self.population[j];
255 if !self.is_evaluated(ind_i) || !self.is_evaluated(ind_j) {
256 return DominanceRelation::NonDominated;
257 }
258 let mut i_dominates = false;
259 let mut j_dominates = false;
260 for k in 0..ind_i.objectives.len().min(self.config.objectives.len()) {
261 let obj_config = &self.config.objectives[k];
262 let val_i = ind_i.objectives[k];
263 let val_j = ind_j.objectives[k];
264 match obj_config.direction {
265 OptimizationDirection::Minimize => {
266 if val_i < val_j {
267 i_dominates = true;
268 } else if val_i > val_j {
269 j_dominates = true;
270 }
271 }
272 OptimizationDirection::Maximize => {
273 if val_i > val_j {
274 i_dominates = true;
275 } else if val_i < val_j {
276 j_dominates = true;
277 }
278 }
279 }
280 }
281 if i_dominates && !j_dominates {
282 DominanceRelation::Dominates
283 } else if j_dominates && !i_dominates {
284 DominanceRelation::DominatedBy
285 } else {
286 DominanceRelation::NonDominated
287 }
288 }
289 pub(super) fn calculate_crowding_distance(&mut self, front: &[usize]) {
294 for &idx in front {
295 self.population[idx].crowding_distance = T::zero();
296 }
297 let front: Vec<usize> = front
298 .iter()
299 .copied()
300 .filter(|&idx| self.is_evaluated(&self.population[idx]))
301 .collect();
302 let front = front.as_slice();
303 let front_size = front.len();
304 if front_size == 0 {
305 return;
306 }
307 if front_size <= 2 {
308 for &idx in front {
309 self.population[idx].crowding_distance = T::infinity();
310 }
311 return;
312 }
313 let num_objectives = self.config.objectives.len();
314 for obj_idx in 0..num_objectives {
315 let mut sorted_front = front.to_vec();
316 sorted_front.sort_by(|&a, &b| {
317 self.population[a].objectives[obj_idx]
318 .partial_cmp(&self.population[b].objectives[obj_idx])
319 .unwrap_or(Ordering::Equal)
320 });
321 self.population[sorted_front[0]].crowding_distance = T::infinity();
322 self.population[sorted_front[front_size - 1]].crowding_distance = T::infinity();
323 let obj_min = self.population[sorted_front[0]].objectives[obj_idx];
324 let obj_max = self.population[sorted_front[front_size - 1]].objectives[obj_idx];
325 let obj_range = obj_max - obj_min;
326 if obj_range > T::zero() {
327 for i in 1..front_size - 1 {
328 let idx = sorted_front[i];
329 let prev_obj = self.population[sorted_front[i - 1]].objectives[obj_idx];
330 let next_obj = self.population[sorted_front[i + 1]].objectives[obj_idx];
331 let distance = (next_obj - prev_obj) / obj_range;
332 self.population[idx].crowding_distance =
333 self.population[idx].crowding_distance + distance;
334 }
335 }
336 }
337 }
338 pub(super) fn environmental_selection(
340 &mut self,
341 combined_population: Vec<Individual<T>>,
342 ) -> Vec<Individual<T>> {
343 self.population = combined_population;
344 let fronts = self.non_dominated_sort();
345 let mut new_population = Vec::new();
346 let mut front_idx = 0;
347 while front_idx < fronts.len() {
348 let front = &fronts[front_idx];
349 if new_population.len() + front.len() <= self.population_size {
350 self.calculate_crowding_distance(front);
351 for &idx in front {
352 new_population.push(self.population[idx].clone());
353 }
354 front_idx += 1;
355 } else {
356 self.calculate_crowding_distance(front);
357 let mut front_individuals: Vec<_> = front
358 .iter()
359 .map(|&idx| (idx, self.population[idx].crowding_distance))
360 .collect();
361 front_individuals.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
362 let remaining_slots = self.population_size - new_population.len();
363 for &(idx, _) in front_individuals.iter().take(remaining_slots) {
364 new_population.push(self.population[idx].clone());
365 }
366 break;
367 }
368 }
369 new_population
370 }
371 pub(super) fn update_pareto_front_from_population(&mut self) {
378 let mut pareto_solutions = Vec::new();
379 for individual in &self.population {
380 if individual.rank == 0 && self.is_evaluated(individual) {
381 let solution = ParetoSolution {
382 architecture: individual.architecture.clone(),
383 objectives: individual.objectives.clone(),
384 constraint_violations: individual.constraints.clone(),
385 rank: individual.rank,
386 crowding_distance: individual.crowding_distance,
387 metadata: SolutionMetadata {
388 id: individual.id.clone(),
389 generation: self.generation,
390 evaluation_count: self.statistics.total_evaluations,
391 parents: Vec::new(),
392 creation_method: creation_method_from_id(
393 &individual.architecture.architecture_id,
394 ),
395 },
396 };
397 pareto_solutions.push(solution);
398 }
399 }
400 self.pareto_front.solutions = pareto_solutions;
401 self.pareto_front.generation = self.generation;
402 self.pareto_front.last_updated = std::time::SystemTime::now();
403 self.update_objective_bounds();
404 self.calculate_front_metrics();
405 }
406 pub(super) fn update_objective_bounds(&mut self) {
407 if self.pareto_front.solutions.is_empty() {
408 return;
409 }
410 let num_objectives = self.pareto_front.solutions[0].objectives.len();
411 let mut min_values = vec![T::infinity(); num_objectives];
412 let mut max_values = vec![T::neg_infinity(); num_objectives];
413 for solution in &self.pareto_front.solutions {
414 for (i, &obj_val) in solution.objectives.iter().enumerate() {
415 if obj_val < min_values[i] {
416 min_values[i] = obj_val;
417 }
418 if obj_val > max_values[i] {
419 max_values[i] = obj_val;
420 }
421 }
422 }
423 self.pareto_front.objective_bounds = ObjectiveBounds {
424 min_values: min_values.clone(),
425 max_values: max_values.clone(),
426 ideal_point: min_values,
427 nadir_point: max_values,
428 };
429 }
430 pub(super) fn calculate_front_metrics(&mut self) {
436 let front = self.front_in_minimization_space();
437 let hypervolume = self.calculate_hypervolume();
438
439 let previous = self.previous_hypervolume;
440 self.previous_hypervolume = Some(hypervolume);
441
442 let directions = self.objective_directions();
443 let reference = self
444 .hypervolume_reference
445 .as_ref()
446 .map(|raw| normalize_reference_for_minimization(raw, &directions))
447 .unwrap_or_default();
448
449 let population_objectives: Vec<Vec<T>> = normalize_front_for_minimization(
452 &self
453 .population
454 .iter()
455 .map(|individual| individual.objectives.clone())
456 .collect::<Vec<_>>(),
457 &directions,
458 );
459
460 self.pareto_front.metrics = metrics::front_metrics_in_minimization_space(
461 &front,
462 &population_objectives,
463 &reference,
464 hypervolume,
465 previous,
466 );
467 let convergence = self.pareto_front.metrics.convergence;
468 let spread = self.pareto_front.metrics.spread;
469 self.statistics.pareto_front_size = self.pareto_front.solutions.len();
470 if hypervolume > self.statistics.best_hypervolume {
472 self.statistics.best_hypervolume = hypervolume;
473 }
474 self.statistics.generation = self.generation;
475 self.statistics.convergence_history.push(convergence);
476 self.statistics.diversity_history.push(spread);
477 }
478 pub(super) fn calculate_hypervolume(&mut self) -> T {
486 let front = self.front_in_minimization_space();
487 if front.is_empty() {
488 return T::zero();
489 }
490 let directions = self.objective_directions();
491
492 if self.hypervolume_reference.is_none() {
493 let Some(derived) = derive_reference_point(&front) else {
496 return T::zero();
497 };
498 self.hypervolume_reference =
499 Some(normalize_reference_for_minimization(&derived, &directions));
500 }
501 let Some(raw_reference) = self.hypervolume_reference.as_ref() else {
502 return T::zero();
503 };
504 let reference = normalize_reference_for_minimization(raw_reference, &directions);
505 hypervolume_minimization(&front, &reference)
506 }
507}
508impl<
509 T: Float
510 + Debug
511 + Default
512 + Clone
513 + Send
514 + Sync
515 + std::fmt::Debug
516 + PartialOrd
517 + std::iter::Sum,
518 > NSGA2<T>
519{
520 pub(super) fn objective_vector_for_result(&self, result: &SearchResult<T>) -> Vec<T> {
525 super::core::objective_vector_for_result(&self.config.objectives, result)
526 }
527 pub(crate) fn select_by_rank_and_crowding(
535 &mut self,
536 results: &[SearchResult<T>],
537 k: usize,
538 ) -> Vec<usize> {
539 if results.is_empty() || k == 0 {
540 return Vec::new();
541 }
542 self.population = results
543 .iter()
544 .enumerate()
545 .map(|(i, result)| Individual {
546 architecture: result.architecture.clone(),
547 objectives: self.objective_vector_for_result(result),
548 constraints: Vec::new(),
549 rank: 0,
550 crowding_distance: T::zero(),
551 fitness: T::zero(),
552 id: format!("sel_{}", i),
553 })
554 .collect();
555 let fronts = self.non_dominated_sort();
556 for front in &fronts {
557 self.calculate_crowding_distance(front);
558 }
559 let mut order: Vec<usize> = (0..self.population.len()).collect();
560 order.sort_by(|&a, &b| {
561 let rank_a = self.population[a].rank;
562 let rank_b = self.population[b].rank;
563 rank_a.cmp(&rank_b).then_with(|| {
564 self.population[b]
565 .crowding_distance
566 .partial_cmp(&self.population[a].crowding_distance)
567 .unwrap_or(Ordering::Equal)
568 })
569 });
570 order.truncate(k.min(self.population.len()));
571 order
572 }
573 pub(crate) fn pareto_front_from_results(
584 &mut self,
585 results: &[SearchResult<T>],
586 ) -> ParetoFront<T> {
587 self.population = results
588 .iter()
589 .enumerate()
590 .map(|(i, result)| {
591 let id = if result.architecture.architecture_id.is_empty() {
592 format!("ind_{}", i)
593 } else {
594 result.architecture.architecture_id.clone()
595 };
596 Individual {
597 architecture: result.architecture.clone(),
598 objectives: self.objective_vector_for_result(result),
599 constraints: Vec::new(),
600 rank: 0,
601 crowding_distance: T::zero(),
602 fitness: T::zero(),
603 id,
604 }
605 })
606 .collect();
607 self.generation += 1;
608 self.statistics.total_evaluations += results.len();
609 let fronts = self.non_dominated_sort();
610 for front in &fronts {
611 self.calculate_crowding_distance(front);
612 }
613 self.update_pareto_front_from_population();
614 self.pareto_front.clone()
615 }
616 pub(crate) fn mean_objective_distance(&self, results: &[SearchResult<T>]) -> f64 {
620 let objective_vectors: Vec<Vec<T>> = results
621 .iter()
622 .map(|r| self.objective_vector_for_result(r))
623 .collect();
624 metrics::mean_pairwise_distance(&objective_vectors)
625 }
626 pub(super) fn tournament_selection(&mut self, tournamentsize: usize) -> Result<Individual<T>> {
627 if self.population.is_empty() {
628 return Err(OptimError::InvalidConfig("Empty population".to_string()));
629 }
630 let mut best_idx = self.rng.gen_range(0..self.population.len());
631 for _ in 1..tournamentsize {
632 let idx = self.rng.gen_range(0..self.population.len());
633 if self.population[idx].rank < self.population[best_idx].rank
634 || (self.population[idx].rank == self.population[best_idx].rank
635 && self.population[idx].crowding_distance
636 > self.population[best_idx].crowding_distance)
637 {
638 best_idx = idx;
639 }
640 }
641 Ok(self.population[best_idx].clone())
642 }
643 pub(super) fn crossover(
649 &mut self,
650 parent1: &Individual<T>,
651 parent2: &Individual<T>,
652 ) -> Result<Individual<T>> {
653 let architecture = operators::crossover_architectures(
654 &mut self.rng,
655 &parent1.architecture,
656 &parent2.architecture,
657 );
658 Ok(Individual {
659 id: architecture.architecture_id.clone(),
660 architecture,
661 objectives: Vec::new(),
662 constraints: Vec::new(),
663 rank: 0,
664 crowding_distance: T::zero(),
665 fitness: T::zero(),
666 })
667 }
668 pub(super) fn mutate(&mut self, individual: &mut Individual<T>) -> Result<()> {
673 let changed = operators::mutate_architecture(
674 &mut self.rng,
675 &mut individual.architecture,
676 NSGA2_COMPONENT_MUTATION_RATE,
677 NSGA2_PARAMETER_MUTATION_RATE,
678 );
679 if changed {
680 individual.objectives.clear();
683 individual.id = individual.architecture.architecture_id.clone();
684 }
685 Ok(())
686 }
687}
688
689pub(super) const NSGA2_COMPONENT_MUTATION_RATE: f64 = 0.2;
691
692pub(super) const NSGA2_PARAMETER_MUTATION_RATE: f64 = 0.3;
694
695fn creation_method_from_id(architecture_id: &str) -> CreationMethod {
700 if architecture_id.starts_with("offspring_") {
701 CreationMethod::Crossover
702 } else if architecture_id.starts_with("mutant_") {
703 CreationMethod::Mutation
704 } else if architecture_id.starts_with("arch_") {
705 CreationMethod::RandomGeneration
706 } else {
707 CreationMethod::Custom
708 }
709}
710impl<
711 T: Float
712 + Debug
713 + Default
714 + Clone
715 + Send
716 + Sync
717 + std::fmt::Debug
718 + PartialOrd
719 + std::iter::Sum,
720 > MultiObjectiveOptimizer<T> for NSGA2<T>
721{
722 fn initialize(&mut self, config: &MultiObjectiveConfig<T>) -> Result<()> {
723 self.config = config.clone();
724 self.initialize_population()?;
725 Ok(())
726 }
727 fn update_pareto_front(&mut self, results: &[SearchResult<T>]) -> Result<ParetoFront<T>> {
740 for result in results {
741 let objectives = self.objective_vector_for_result(result);
742 let architecture_id = result.architecture.architecture_id.as_str();
743 let existing = if architecture_id.is_empty() {
744 None
745 } else {
746 self.population
747 .iter()
748 .position(|ind| ind.architecture.architecture_id == architecture_id)
749 };
750 match existing {
751 Some(index) => {
752 self.population[index].objectives = objectives;
753 self.population[index].architecture = result.architecture.clone();
754 }
755 None => {
756 let vacancy = self
759 .population
760 .iter()
761 .position(|ind| !self.is_evaluated(ind));
762 let id = if architecture_id.is_empty() {
763 format!("ind_{}", self.population.len())
764 } else {
765 architecture_id.to_string()
766 };
767 match vacancy {
768 Some(index) => {
769 self.population[index].objectives = objectives;
770 self.population[index].architecture = result.architecture.clone();
771 self.population[index].id = id;
772 }
773 None => self.population.push(Individual {
774 id,
775 architecture: result.architecture.clone(),
776 objectives,
777 constraints: Vec::new(),
778 rank: 0,
779 crowding_distance: T::zero(),
780 fitness: T::zero(),
781 }),
782 }
783 }
784 }
785 }
786 if self.population.len() > self.population_size {
800 let combined = std::mem::take(&mut self.population);
801 self.population = self.environmental_selection(combined);
802 }
803
804 self.generation += 1;
805 self.statistics.total_evaluations += results.len();
806 let fronts = self.non_dominated_sort();
807 for front in &fronts {
808 self.calculate_crowding_distance(front);
809 }
810 self.update_pareto_front_from_population();
811 Ok(self.pareto_front.clone())
812 }
813 fn get_pareto_front(&self) -> &ParetoFront<T> {
814 &self.pareto_front
815 }
816 fn select_candidates(
817 &mut self,
818 _population: &[OptimizerArchitecture<T>],
819 _objectives: &[T],
820 ) -> Result<Vec<OptimizerArchitecture<T>>> {
821 let mut new_population = Vec::new();
822 for _ in 0..self.population_size {
823 let parent1 = self.tournament_selection(2)?;
824 let parent2 = self.tournament_selection(2)?;
825 let mut offspring = if self.rng.gen_range(0.0..1.0) < self.crossover_prob {
826 self.crossover(&parent1, &parent2)?
827 } else {
828 parent1.clone()
829 };
830 if self.rng.gen_range(0.0..1.0) < self.mutation_prob {
831 self.mutate(&mut offspring)?;
832 }
833 new_population.push(offspring.architecture);
834 }
835 Ok(new_population)
836 }
837 fn name(&self) -> &str {
838 "NSGA-II"
839 }
840 fn get_statistics(&self) -> MultiObjectiveStatistics<T> {
841 self.statistics.clone()
842 }
843}