1use crate::error::{OptimError, Result};
33use crate::nas_engine::{MultiObjectiveConfig, OptimizerArchitecture, SearchResult};
34use scirs2_core::numeric::Float;
35use scirs2_core::random::Random;
36use std::collections::{HashMap, VecDeque};
37use std::fmt::Debug;
38
39use super::core::{
40 objective_directions, objective_vector_for_result, CreationMethod, Individual,
41 MultiObjectiveOptimizer, MultiObjectiveStatistics, ObjectiveBounds, ParetoFront,
42 ParetoSolution, SolutionMetadata,
43};
44use super::decomposition::{
45 das_dennis_weights, divisions_for_at_least, scalarize, update_ideal_point,
46 weight_neighborhoods, Scalarization,
47};
48use super::hypervolume::{
49 derive_reference_point, hypervolume_minimization, normalize_front_for_minimization,
50 normalize_reference_for_minimization,
51};
52use super::metrics;
53use super::operators;
54
55pub const DEFAULT_SUBPROBLEM_TARGET: usize = 20;
61
62pub const DEFAULT_NEIGHBORHOOD_SIZE: usize = 5;
64
65pub const NEIGHBORHOOD_SELECTION_PROB: f64 = 0.9;
68
69pub const MAX_REPLACEMENTS: usize = 2;
73
74pub const DEFAULT_ARCHIVE_CAPACITY: usize = 100;
76
77const MOEAD_COMPONENT_MUTATION_RATE: f64 = 0.2;
79
80const MOEAD_PARAMETER_MUTATION_RATE: f64 = 0.3;
82
83const MOEAD_CROSSOVER_PROB: f64 = 0.9;
85
86const MOEAD_MUTATION_PROB: f64 = 0.3;
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum DecompositionMethod {
92 WeightedSum,
94 Tchebycheff,
96 PBI,
98 ASF,
100}
101
102impl DecompositionMethod {
103 pub fn scalarization(self) -> Scalarization {
105 match self {
106 DecompositionMethod::WeightedSum => Scalarization::WeightedSum,
107 DecompositionMethod::Tchebycheff => Scalarization::Tchebycheff,
108 DecompositionMethod::PBI => Scalarization::PenaltyBoundaryIntersection,
109 DecompositionMethod::ASF => Scalarization::AchievementScalarizing,
110 }
111 }
112}
113
114pub struct MOEADOptimizer<T: Float + Debug + Send + Sync + 'static> {
116 pub(super) config: MultiObjectiveConfig<T>,
118 pub(super) weight_vectors: Vec<Vec<T>>,
120 pub(super) population: Vec<Individual<T>>,
123 pub(super) neighbors: Vec<Vec<usize>>,
125 pub(super) pareto_front: ParetoFront<T>,
127 pub(super) ideal_point: Vec<T>,
129 pub(super) decomposition: DecompositionMethod,
131 pub(super) neighborhood_size: usize,
133 pub(super) generation: usize,
135 pub(super) statistics: MultiObjectiveStatistics<T>,
137 pub(super) divisions: usize,
139 pub(super) subproblem_target: usize,
141 pub(super) archive: Vec<Individual<T>>,
143 pub(super) archive_capacity: usize,
145 pub(super) subproblem_of: HashMap<String, usize>,
149 pub(super) subproblem_order: VecDeque<String>,
151 pub(super) rng: Random<scirs2_core::random::rngs::StdRng>,
153 pub(super) hypervolume_reference: Option<Vec<T>>,
155 pub(super) previous_hypervolume: Option<T>,
157 pub(super) last_replacements: usize,
159}
160
161impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> MOEADOptimizer<T> {
162 pub fn new(config: MultiObjectiveConfig<T>) -> Result<Self> {
171 Self::with_settings(
172 config,
173 DEFAULT_SUBPROBLEM_TARGET,
174 DecompositionMethod::Tchebycheff,
175 DEFAULT_NEIGHBORHOOD_SIZE,
176 scirs2_core::random::random::<u64>(),
177 )
178 }
179
180 pub fn with_seed(config: MultiObjectiveConfig<T>, seed: u64) -> Result<Self> {
182 Self::with_settings(
183 config,
184 DEFAULT_SUBPROBLEM_TARGET,
185 DecompositionMethod::Tchebycheff,
186 DEFAULT_NEIGHBORHOOD_SIZE,
187 seed,
188 )
189 }
190
191 pub fn with_settings(
193 config: MultiObjectiveConfig<T>,
194 subproblem_target: usize,
195 decomposition: DecompositionMethod,
196 neighborhood_size: usize,
197 seed: u64,
198 ) -> Result<Self> {
199 let mut optimizer = Self {
200 config: config.clone(),
201 weight_vectors: Vec::new(),
202 population: Vec::new(),
203 neighbors: Vec::new(),
204 pareto_front: ParetoFront::default(),
205 ideal_point: Vec::new(),
206 decomposition,
207 neighborhood_size: neighborhood_size.max(1),
208 generation: 0,
209 statistics: MultiObjectiveStatistics::default(),
210 divisions: 0,
211 subproblem_target: subproblem_target.max(1),
212 archive: Vec::new(),
213 archive_capacity: DEFAULT_ARCHIVE_CAPACITY,
214 subproblem_of: HashMap::new(),
215 subproblem_order: VecDeque::new(),
216 rng: Random::seed(seed),
217 hypervolume_reference: None,
218 previous_hypervolume: None,
219 last_replacements: 0,
220 };
221 if !config.objectives.is_empty() {
222 optimizer.build_decomposition(&config)?;
223 }
224 Ok(optimizer)
225 }
226
227 pub fn decomposition(&self) -> DecompositionMethod {
229 self.decomposition
230 }
231
232 pub fn set_decomposition(&mut self, decomposition: DecompositionMethod) {
235 self.decomposition = decomposition;
236 }
237
238 pub fn subproblem_count(&self) -> usize {
240 self.weight_vectors.len()
241 }
242
243 pub fn weight_vector(&self, index: usize) -> Option<&[T]> {
245 self.weight_vectors
246 .get(index)
247 .map(|weight| weight.as_slice())
248 }
249
250 pub fn neighborhood(&self, index: usize) -> Option<&[usize]> {
252 self.neighbors.get(index).map(|list| list.as_slice())
253 }
254
255 pub fn ideal_point(&self) -> &[T] {
257 &self.ideal_point
258 }
259
260 pub fn set_archive_capacity(&mut self, capacity: usize) {
262 self.archive_capacity = capacity.max(1);
263 self.truncate_archive();
264 }
265
266 pub fn set_hypervolume_reference(&mut self, reference: Vec<T>) -> Result<()> {
269 if reference.len() != self.config.objectives.len() {
270 return Err(OptimError::InvalidConfig(format!(
271 "hypervolume reference has {} entries but {} objectives are configured",
272 reference.len(),
273 self.config.objectives.len()
274 )));
275 }
276 self.hypervolume_reference = Some(reference);
277 Ok(())
278 }
279
280 fn build_decomposition(&mut self, config: &MultiObjectiveConfig<T>) -> Result<()> {
283 let num_objectives = config.objectives.len();
284 if num_objectives == 0 {
285 return Err(OptimError::InvalidConfig(
286 "MOEA/D needs at least one configured objective to decompose".to_string(),
287 ));
288 }
289 let (divisions, lattice_size) =
290 divisions_for_at_least(num_objectives, self.subproblem_target).ok_or_else(|| {
291 OptimError::InvalidConfig(format!(
292 "no Das-Dennis lattice with at least {} vectors exists for {} objectives",
293 self.subproblem_target, num_objectives
294 ))
295 })?;
296 self.divisions = divisions;
297 self.weight_vectors = das_dennis_weights::<T>(num_objectives, divisions);
298 if self.weight_vectors.len() != lattice_size {
299 return Err(OptimError::OptimizationError(format!(
300 "weight lattice size {} disagrees with the computed {}",
301 self.weight_vectors.len(),
302 lattice_size
303 )));
304 }
305 self.neighbors = weight_neighborhoods(&self.weight_vectors, self.neighborhood_size);
306 self.ideal_point = vec![T::infinity(); num_objectives];
307 self.population.clear();
308 self.subproblem_of.clear();
309 self.subproblem_order.clear();
310 for index in 0..self.weight_vectors.len() {
311 let architecture = operators::sample_architecture::<T>(&mut self.rng, "arch");
312 let id = architecture.architecture_id.clone();
313 self.population.push(Individual {
314 architecture,
315 objectives: Vec::new(),
316 constraints: Vec::new(),
317 rank: 0,
318 crowding_distance: T::zero(),
319 fitness: T::zero(),
320 id: id.clone(),
321 });
322 self.remember_subproblem(id, index);
323 }
324 Ok(())
325 }
326
327 fn remember_subproblem(&mut self, architecture_id: String, index: usize) {
330 if architecture_id.is_empty() {
331 return;
332 }
333 let capacity = (self.weight_vectors.len().max(1)).saturating_mul(4);
334 if self
335 .subproblem_of
336 .insert(architecture_id.clone(), index)
337 .is_none()
338 {
339 self.subproblem_order.push_back(architecture_id);
340 }
341 while self.subproblem_order.len() > capacity {
342 if let Some(oldest) = self.subproblem_order.pop_front() {
343 self.subproblem_of.remove(&oldest);
344 }
345 }
346 }
347
348 fn minimization_objectives(&self, result: &SearchResult<T>) -> Vec<T> {
350 let raw = objective_vector_for_result(&self.config.objectives, result);
351 self.to_minimization(&raw)
352 }
353
354 fn to_minimization(&self, raw: &[T]) -> Vec<T> {
356 let directions = objective_directions(&self.config.objectives);
357 normalize_front_for_minimization(&[raw.to_vec()], &directions)
358 .into_iter()
359 .next()
360 .unwrap_or_default()
361 }
362
363 fn effective_ideal(&self, reference: &[Vec<T>]) -> Vec<T> {
373 let num_objectives = self.config.objectives.len();
374 let mut ideal = self.ideal_point.clone();
375 if ideal.len() < num_objectives {
376 ideal.resize(num_objectives, T::infinity());
377 }
378 for point in reference {
379 update_ideal_point(&mut ideal, point);
380 }
381 for value in ideal.iter_mut() {
382 if !value.is_finite() {
383 *value = T::zero();
384 }
385 }
386 ideal
387 }
388
389 fn subproblem_cost(&self, index: usize, minimized: &[T], ideal: &[T]) -> Option<T> {
392 let weight = self.weight_vectors.get(index)?;
393 Some(scalarize(
394 self.decomposition.scalarization(),
395 minimized,
396 weight,
397 ideal,
398 ))
399 }
400
401 fn best_fit_subproblem(&self, minimized: &[T], ideal: &[T]) -> Option<usize> {
407 let mut best_index = None;
408 let mut best_cost = T::infinity();
409 for index in 0..self.weight_vectors.len() {
410 let Some(cost) = self.subproblem_cost(index, minimized, ideal) else {
411 continue;
412 };
413 if cost < best_cost {
414 best_cost = cost;
415 best_index = Some(index);
416 }
417 }
418 best_index
419 }
420
421 fn is_evaluated(&self, individual: &Individual<T>) -> bool {
423 !self.config.objectives.is_empty()
424 && individual.objectives.len() == self.config.objectives.len()
425 }
426
427 fn dominates(a: &[T], b: &[T]) -> bool {
429 if a.len() != b.len() || a.is_empty() {
430 return false;
431 }
432 let mut strictly_better = false;
433 for (left, right) in a.iter().zip(b.iter()) {
434 if *left > *right {
435 return false;
436 }
437 if *left < *right {
438 strictly_better = true;
439 }
440 }
441 strictly_better
442 }
443
444 fn offer_to_archive(&mut self, candidate: &Individual<T>, minimized: &[T]) -> bool {
452 if minimized.is_empty() {
453 return false;
454 }
455 let member_objectives: Vec<Vec<T>> = self
456 .archive
457 .iter()
458 .map(|member| self.to_minimization(&member.objectives))
459 .collect();
460
461 let already_covered = member_objectives
462 .iter()
463 .any(|member| Self::dominates(member, minimized) || member.as_slice() == minimized);
464 if already_covered {
465 return false;
466 }
467
468 let candidate_id = candidate.architecture.architecture_id.as_str();
469 let mut index = 0usize;
470 self.archive.retain(|member| {
471 let dominated = Self::dominates(minimized, &member_objectives[index]);
472 let superseded =
475 !candidate_id.is_empty() && member.architecture.architecture_id == candidate_id;
476 index += 1;
477 !dominated && !superseded
478 });
479 self.archive.push(candidate.clone());
480 self.truncate_archive();
481 true
482 }
483
484 fn truncate_archive(&mut self) {
489 while self.archive.len() > self.archive_capacity {
490 let points: Vec<Vec<T>> = self
491 .archive
492 .iter()
493 .map(|member| self.to_minimization(&member.objectives))
494 .collect();
495 let Some(victim) = most_crowded_index(&points) else {
496 break;
497 };
498 self.archive.remove(victim);
499 }
500 }
501
502 fn publish_archive(&mut self) {
505 let solutions: Vec<ParetoSolution<T>> = self
506 .archive
507 .iter()
508 .map(|member| ParetoSolution {
509 architecture: member.architecture.clone(),
510 objectives: member.objectives.clone(),
511 constraint_violations: member.constraints.clone(),
512 rank: 0,
513 crowding_distance: member.crowding_distance,
514 metadata: SolutionMetadata {
515 id: member.id.clone(),
516 generation: self.generation,
517 evaluation_count: self.statistics.total_evaluations,
518 parents: Vec::new(),
519 creation_method: creation_method_from_id(&member.architecture.architecture_id),
520 },
521 })
522 .collect();
523 self.pareto_front.solutions = solutions;
524 self.pareto_front.generation = self.generation;
525 self.pareto_front.last_updated = std::time::SystemTime::now();
526 self.update_objective_bounds();
527 self.update_front_metrics();
528 }
529
530 fn update_objective_bounds(&mut self) {
531 let Some(first) = self.pareto_front.solutions.first() else {
532 return;
533 };
534 let num_objectives = first.objectives.len();
535 let mut min_values = vec![T::infinity(); num_objectives];
536 let mut max_values = vec![T::neg_infinity(); num_objectives];
537 for solution in &self.pareto_front.solutions {
538 for (index, value) in solution.objectives.iter().enumerate() {
539 if index >= num_objectives {
540 break;
541 }
542 if *value < min_values[index] {
543 min_values[index] = *value;
544 }
545 if *value > max_values[index] {
546 max_values[index] = *value;
547 }
548 }
549 }
550 self.pareto_front.objective_bounds = ObjectiveBounds {
551 min_values: min_values.clone(),
552 max_values: max_values.clone(),
553 ideal_point: min_values,
554 nadir_point: max_values,
555 };
556 }
557
558 fn front_in_minimization_space(&self) -> Vec<Vec<T>> {
560 let directions = objective_directions(&self.config.objectives);
561 let raw: Vec<Vec<T>> = self
562 .pareto_front
563 .solutions
564 .iter()
565 .map(|solution| solution.objectives.clone())
566 .collect();
567 normalize_front_for_minimization(&raw, &directions)
568 }
569
570 fn calculate_hypervolume(&mut self) -> T {
573 let front = self.front_in_minimization_space();
574 if front.is_empty() {
575 return T::zero();
576 }
577 let directions = objective_directions(&self.config.objectives);
578 if self.hypervolume_reference.is_none() {
579 let Some(derived) = derive_reference_point(&front) else {
580 return T::zero();
581 };
582 self.hypervolume_reference =
585 Some(normalize_reference_for_minimization(&derived, &directions));
586 }
587 let Some(raw_reference) = self.hypervolume_reference.as_ref() else {
588 return T::zero();
589 };
590 let reference = normalize_reference_for_minimization(raw_reference, &directions);
591 hypervolume_minimization(&front, &reference)
592 }
593
594 fn update_front_metrics(&mut self) {
595 let front = self.front_in_minimization_space();
596 let hypervolume = self.calculate_hypervolume();
597 let previous = self.previous_hypervolume;
598 self.previous_hypervolume = Some(hypervolume);
599
600 let directions = objective_directions(&self.config.objectives);
601 let reference = self
602 .hypervolume_reference
603 .as_ref()
604 .map(|raw| normalize_reference_for_minimization(raw, &directions))
605 .unwrap_or_default();
606 let population: Vec<Vec<T>> = normalize_front_for_minimization(
607 &self
608 .population
609 .iter()
610 .map(|individual| individual.objectives.clone())
611 .collect::<Vec<_>>(),
612 &directions,
613 );
614
615 self.pareto_front.metrics = metrics::front_metrics_in_minimization_space(
616 &front,
617 &population,
618 &reference,
619 hypervolume,
620 previous,
621 );
622 self.statistics.pareto_front_size = self.pareto_front.solutions.len();
623 if hypervolume > self.statistics.best_hypervolume {
624 self.statistics.best_hypervolume = hypervolume;
625 }
626 self.statistics.generation = self.generation;
627 self.statistics
628 .convergence_history
629 .push(self.pareto_front.metrics.convergence);
630 self.statistics
631 .diversity_history
632 .push(self.pareto_front.metrics.spread);
633 let solved = self
634 .population
635 .iter()
636 .filter(|individual| self.is_evaluated(individual))
637 .count();
638 self.statistics.algorithm_metrics.insert(
639 "subproblems".to_string(),
640 T::from(self.weight_vectors.len()).unwrap_or_else(T::zero),
641 );
642 self.statistics.algorithm_metrics.insert(
643 "solved_subproblems".to_string(),
644 T::from(solved).unwrap_or_else(T::zero),
645 );
646 self.statistics.algorithm_metrics.insert(
647 "archive_size".to_string(),
648 T::from(self.archive.len()).unwrap_or_else(T::zero),
649 );
650 self.statistics.algorithm_metrics.insert(
651 "replacements".to_string(),
652 T::from(self.last_replacements).unwrap_or_else(T::zero),
653 );
654 }
655
656 pub fn last_replacements(&self) -> usize {
658 self.last_replacements
659 }
660
661 pub fn archive(&self) -> &[Individual<T>] {
663 &self.archive
664 }
665
666 pub(crate) fn select_by_decomposition(
674 &self,
675 results: &[SearchResult<T>],
676 k: usize,
677 ) -> Vec<usize> {
678 if results.is_empty() || k == 0 || self.weight_vectors.is_empty() {
679 return Vec::new();
680 }
681 let minimized_results: Vec<Vec<T>> = results
682 .iter()
683 .map(|result| self.minimization_objectives(result))
684 .collect();
685 let ideal = self.effective_ideal(&minimized_results);
689
690 let mut scored: Vec<(usize, usize, T)> = Vec::with_capacity(results.len());
691 for (index, minimized) in minimized_results.iter().enumerate() {
692 let Some(subproblem) = self.best_fit_subproblem(minimized, &ideal) else {
693 continue;
694 };
695 let cost = self
696 .subproblem_cost(subproblem, minimized, &ideal)
697 .unwrap_or_else(T::infinity);
698 scored.push((index, subproblem, cost));
699 }
700 scored.sort_by(|a, b| {
701 a.2.partial_cmp(&b.2)
702 .unwrap_or(std::cmp::Ordering::Equal)
703 .then_with(|| a.0.cmp(&b.0))
704 });
705
706 let mut taken_subproblems = std::collections::HashSet::new();
707 let mut selected = Vec::with_capacity(k.min(scored.len()));
708 for (index, subproblem, _) in &scored {
709 if selected.len() >= k {
710 break;
711 }
712 if taken_subproblems.insert(*subproblem) {
713 selected.push(*index);
714 }
715 }
716 for (index, _, _) in &scored {
717 if selected.len() >= k {
718 break;
719 }
720 if !selected.contains(index) {
721 selected.push(*index);
722 }
723 }
724 selected
725 }
726
727 pub(crate) fn mean_objective_distance(&self, results: &[SearchResult<T>]) -> f64 {
730 let vectors: Vec<Vec<T>> = results
731 .iter()
732 .map(|result| objective_vector_for_result(&self.config.objectives, result))
733 .collect();
734 metrics::mean_pairwise_distance(&vectors)
735 }
736}
737
738fn most_crowded_index<T: Float>(points: &[Vec<T>]) -> Option<usize> {
754 if points.len() < 2 {
755 return None;
756 }
757 let dims = points.iter().map(|point| point.len()).min()?;
758 if dims == 0 {
759 return None;
760 }
761 let mut min_values = vec![T::infinity(); dims];
762 let mut max_values = vec![T::neg_infinity(); dims];
763 for point in points {
764 for index in 0..dims {
765 if point[index] < min_values[index] {
766 min_values[index] = point[index];
767 }
768 if point[index] > max_values[index] {
769 max_values[index] = point[index];
770 }
771 }
772 }
773 let spans: Vec<T> = (0..dims)
774 .map(|index| {
775 let span = max_values[index] - min_values[index];
776 if span > T::zero() {
777 span
778 } else {
779 T::one()
780 }
781 })
782 .collect();
783
784 let sorted_distances = |i: usize| -> Vec<T> {
785 let point = &points[i];
786 let mut distances: Vec<T> = points
787 .iter()
788 .enumerate()
789 .filter(|(j, _)| *j != i)
790 .map(|(_, other)| {
791 let mut sum = T::zero();
792 for index in 0..dims {
793 let diff = (point[index] - other[index]) / spans[index];
794 sum = sum + diff * diff;
795 }
796 sum.sqrt()
797 })
798 .collect();
799 distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
800 distances
801 };
802
803 let mut worst_index = 0usize;
804 let mut worst_profile = sorted_distances(0);
805 for i in 1..points.len() {
806 let profile = sorted_distances(i);
807 let mut smaller = false;
808 for (candidate, incumbent) in profile.iter().zip(worst_profile.iter()) {
809 if *candidate < *incumbent {
810 smaller = true;
811 break;
812 }
813 if *candidate > *incumbent {
814 break;
815 }
816 }
817 if smaller {
818 worst_index = i;
819 worst_profile = profile;
820 }
821 }
822 Some(worst_index)
823}
824
825fn creation_method_from_id(architecture_id: &str) -> CreationMethod {
828 if architecture_id.starts_with("offspring_") {
829 CreationMethod::Crossover
830 } else if architecture_id.starts_with("mutant_") {
831 CreationMethod::Mutation
832 } else if architecture_id.starts_with("arch_") {
833 CreationMethod::RandomGeneration
834 } else {
835 CreationMethod::Custom
836 }
837}
838
839impl<
840 T: Float
841 + Debug
842 + Default
843 + Clone
844 + Send
845 + Sync
846 + std::fmt::Debug
847 + PartialOrd
848 + std::iter::Sum,
849 > MultiObjectiveOptimizer<T> for MOEADOptimizer<T>
850{
851 fn initialize(&mut self, config: &MultiObjectiveConfig<T>) -> Result<()> {
852 self.config = config.clone();
853 self.generation = 0;
854 self.statistics = MultiObjectiveStatistics::default();
855 self.archive.clear();
856 self.pareto_front = ParetoFront::default();
857 self.hypervolume_reference = None;
858 self.previous_hypervolume = None;
859 self.last_replacements = 0;
860 self.build_decomposition(config)
861 }
862
863 fn update_pareto_front(&mut self, results: &[SearchResult<T>]) -> Result<ParetoFront<T>> {
867 if self.weight_vectors.is_empty() {
868 return Err(OptimError::InvalidConfig(
869 "MOEA/D has no weight vectors; call initialize with a configuration \
870 that declares at least one objective first"
871 .to_string(),
872 ));
873 }
874 self.last_replacements = 0;
875 for result in results {
876 let raw = objective_vector_for_result(&self.config.objectives, result);
877 let minimized = self.to_minimization(&raw);
878 if minimized.len() != self.config.objectives.len() {
879 continue;
880 }
881 update_ideal_point(&mut self.ideal_point, &minimized);
882
883 let ideal = self.effective_ideal(&[]);
884 let architecture_id = result.architecture.architecture_id.clone();
885 let owner = self
886 .subproblem_of
887 .get(&architecture_id)
888 .copied()
889 .or_else(|| self.best_fit_subproblem(&minimized, &ideal));
890 let Some(owner) = owner else {
891 continue;
892 };
893 self.subproblem_of.remove(&architecture_id);
895
896 let candidate = Individual {
897 architecture: result.architecture.clone(),
898 objectives: raw,
899 constraints: Vec::new(),
900 rank: 0,
901 crowding_distance: T::zero(),
902 fitness: T::zero(),
903 id: if architecture_id.is_empty() {
904 format!("moead_{}_{}", self.generation, owner)
905 } else {
906 architecture_id
907 },
908 };
909
910 let owner_holds_candidate = self.population.get(owner).is_some_and(|incumbent| {
918 !candidate.architecture.architecture_id.is_empty()
919 && incumbent.architecture.architecture_id
920 == candidate.architecture.architecture_id
921 && !self.is_evaluated(incumbent)
922 });
923 if owner_holds_candidate {
924 self.population[owner] = candidate.clone();
925 self.offer_to_archive(&candidate, &minimized);
926 continue;
927 }
928
929 let mut neighborhood = self
932 .neighbors
933 .get(owner)
934 .cloned()
935 .unwrap_or_else(|| vec![owner]);
936 for position in (1..neighborhood.len()).rev() {
937 let other = self.rng.gen_range(0..=position);
938 neighborhood.swap(position, other);
939 }
940 let mut replacements = 0usize;
941 for neighbor in neighborhood {
942 if replacements >= MAX_REPLACEMENTS {
943 break;
944 }
945 let Some(incumbent) = self.population.get(neighbor) else {
946 continue;
947 };
948 let accept = if self.is_evaluated(incumbent) {
949 let incumbent_min = self.to_minimization(&incumbent.objectives);
950 match (
951 self.subproblem_cost(neighbor, &minimized, &ideal),
952 self.subproblem_cost(neighbor, &incumbent_min, &ideal),
953 ) {
954 (Some(new_cost), Some(old_cost)) => new_cost < old_cost,
955 _ => false,
956 }
957 } else {
958 true
963 };
964 if accept {
965 self.population[neighbor] = candidate.clone();
966 replacements += 1;
967 }
968 }
969 self.last_replacements += replacements;
970 self.offer_to_archive(&candidate, &minimized);
971 }
972 self.generation += 1;
973 self.statistics.total_evaluations += results.len();
974 self.publish_archive();
975 Ok(self.pareto_front.clone())
976 }
977
978 fn get_pareto_front(&self) -> &ParetoFront<T> {
979 &self.pareto_front
980 }
981
982 fn select_candidates(
993 &mut self,
994 population: &[OptimizerArchitecture<T>],
995 _objectives: &[T],
996 ) -> Result<Vec<OptimizerArchitecture<T>>> {
997 if self.weight_vectors.is_empty() {
998 return Err(OptimError::InvalidConfig(
999 "MOEA/D has no weight vectors; call initialize with a configuration \
1000 that declares at least one objective first"
1001 .to_string(),
1002 ));
1003 }
1004 let subproblems = self.weight_vectors.len();
1005 let mut offspring = Vec::with_capacity(subproblems);
1006 for index in 0..subproblems {
1007 let use_neighborhood = self.rng.gen_range(0.0..1.0) < NEIGHBORHOOD_SELECTION_PROB;
1008 let pool: Vec<usize> = if use_neighborhood {
1009 self.neighbors
1010 .get(index)
1011 .cloned()
1012 .unwrap_or_else(|| vec![index])
1013 } else {
1014 (0..self.population.len()).collect()
1015 };
1016 if pool.is_empty() {
1017 continue;
1018 }
1019 let first = pool[self.rng.gen_range(0..pool.len())];
1020 let second = pool[self.rng.gen_range(0..pool.len())];
1021 let parent1 = self
1022 .population
1023 .get(first)
1024 .map(|individual| individual.architecture.clone());
1025 let parent2 = if !population.is_empty() && !use_neighborhood {
1027 Some(population[self.rng.gen_range(0..population.len())].clone())
1028 } else {
1029 self.population
1030 .get(second)
1031 .map(|individual| individual.architecture.clone())
1032 };
1033 let (Some(parent1), Some(parent2)) = (parent1, parent2) else {
1034 continue;
1035 };
1036
1037 let mut child = if self.rng.gen_range(0.0..1.0) < MOEAD_CROSSOVER_PROB {
1038 operators::crossover_architectures(&mut self.rng, &parent1, &parent2)
1039 } else {
1040 parent1
1041 };
1042 if self.rng.gen_range(0.0..1.0) < MOEAD_MUTATION_PROB {
1043 operators::mutate_architecture(
1044 &mut self.rng,
1045 &mut child,
1046 MOEAD_COMPONENT_MUTATION_RATE,
1047 MOEAD_PARAMETER_MUTATION_RATE,
1048 );
1049 }
1050 self.remember_subproblem(child.architecture_id.clone(), index);
1051 offspring.push(child);
1052 }
1053 Ok(offspring)
1054 }
1055
1056 fn name(&self) -> &str {
1057 "MOEA/D"
1058 }
1059
1060 fn get_statistics(&self) -> MultiObjectiveStatistics<T> {
1061 self.statistics.clone()
1062 }
1063}