use crate::error::{OptimError, Result};
use crate::nas_engine::{MultiObjectiveConfig, OptimizerArchitecture, SearchResult};
use scirs2_core::numeric::Float;
use scirs2_core::random::Random;
use std::collections::{HashMap, VecDeque};
use std::fmt::Debug;
use super::core::{
objective_directions, objective_vector_for_result, CreationMethod, Individual,
MultiObjectiveOptimizer, MultiObjectiveStatistics, ObjectiveBounds, ParetoFront,
ParetoSolution, SolutionMetadata,
};
use super::decomposition::{
das_dennis_weights, divisions_for_at_least, scalarize, update_ideal_point,
weight_neighborhoods, Scalarization,
};
use super::hypervolume::{
derive_reference_point, hypervolume_minimization, normalize_front_for_minimization,
normalize_reference_for_minimization,
};
use super::metrics;
use super::operators;
pub const DEFAULT_SUBPROBLEM_TARGET: usize = 20;
pub const DEFAULT_NEIGHBORHOOD_SIZE: usize = 5;
pub const NEIGHBORHOOD_SELECTION_PROB: f64 = 0.9;
pub const MAX_REPLACEMENTS: usize = 2;
pub const DEFAULT_ARCHIVE_CAPACITY: usize = 100;
const MOEAD_COMPONENT_MUTATION_RATE: f64 = 0.2;
const MOEAD_PARAMETER_MUTATION_RATE: f64 = 0.3;
const MOEAD_CROSSOVER_PROB: f64 = 0.9;
const MOEAD_MUTATION_PROB: f64 = 0.3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecompositionMethod {
WeightedSum,
Tchebycheff,
PBI,
ASF,
}
impl DecompositionMethod {
pub fn scalarization(self) -> Scalarization {
match self {
DecompositionMethod::WeightedSum => Scalarization::WeightedSum,
DecompositionMethod::Tchebycheff => Scalarization::Tchebycheff,
DecompositionMethod::PBI => Scalarization::PenaltyBoundaryIntersection,
DecompositionMethod::ASF => Scalarization::AchievementScalarizing,
}
}
}
pub struct MOEADOptimizer<T: Float + Debug + Send + Sync + 'static> {
pub(super) config: MultiObjectiveConfig<T>,
pub(super) weight_vectors: Vec<Vec<T>>,
pub(super) population: Vec<Individual<T>>,
pub(super) neighbors: Vec<Vec<usize>>,
pub(super) pareto_front: ParetoFront<T>,
pub(super) ideal_point: Vec<T>,
pub(super) decomposition: DecompositionMethod,
pub(super) neighborhood_size: usize,
pub(super) generation: usize,
pub(super) statistics: MultiObjectiveStatistics<T>,
pub(super) divisions: usize,
pub(super) subproblem_target: usize,
pub(super) archive: Vec<Individual<T>>,
pub(super) archive_capacity: usize,
pub(super) subproblem_of: HashMap<String, usize>,
pub(super) subproblem_order: VecDeque<String>,
pub(super) rng: Random<scirs2_core::random::rngs::StdRng>,
pub(super) hypervolume_reference: Option<Vec<T>>,
pub(super) previous_hypervolume: Option<T>,
pub(super) last_replacements: usize,
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> MOEADOptimizer<T> {
pub fn new(config: MultiObjectiveConfig<T>) -> Result<Self> {
Self::with_settings(
config,
DEFAULT_SUBPROBLEM_TARGET,
DecompositionMethod::Tchebycheff,
DEFAULT_NEIGHBORHOOD_SIZE,
scirs2_core::random::random::<u64>(),
)
}
pub fn with_seed(config: MultiObjectiveConfig<T>, seed: u64) -> Result<Self> {
Self::with_settings(
config,
DEFAULT_SUBPROBLEM_TARGET,
DecompositionMethod::Tchebycheff,
DEFAULT_NEIGHBORHOOD_SIZE,
seed,
)
}
pub fn with_settings(
config: MultiObjectiveConfig<T>,
subproblem_target: usize,
decomposition: DecompositionMethod,
neighborhood_size: usize,
seed: u64,
) -> Result<Self> {
let mut optimizer = Self {
config: config.clone(),
weight_vectors: Vec::new(),
population: Vec::new(),
neighbors: Vec::new(),
pareto_front: ParetoFront::default(),
ideal_point: Vec::new(),
decomposition,
neighborhood_size: neighborhood_size.max(1),
generation: 0,
statistics: MultiObjectiveStatistics::default(),
divisions: 0,
subproblem_target: subproblem_target.max(1),
archive: Vec::new(),
archive_capacity: DEFAULT_ARCHIVE_CAPACITY,
subproblem_of: HashMap::new(),
subproblem_order: VecDeque::new(),
rng: Random::seed(seed),
hypervolume_reference: None,
previous_hypervolume: None,
last_replacements: 0,
};
if !config.objectives.is_empty() {
optimizer.build_decomposition(&config)?;
}
Ok(optimizer)
}
pub fn decomposition(&self) -> DecompositionMethod {
self.decomposition
}
pub fn set_decomposition(&mut self, decomposition: DecompositionMethod) {
self.decomposition = decomposition;
}
pub fn subproblem_count(&self) -> usize {
self.weight_vectors.len()
}
pub fn weight_vector(&self, index: usize) -> Option<&[T]> {
self.weight_vectors
.get(index)
.map(|weight| weight.as_slice())
}
pub fn neighborhood(&self, index: usize) -> Option<&[usize]> {
self.neighbors.get(index).map(|list| list.as_slice())
}
pub fn ideal_point(&self) -> &[T] {
&self.ideal_point
}
pub fn set_archive_capacity(&mut self, capacity: usize) {
self.archive_capacity = capacity.max(1);
self.truncate_archive();
}
pub fn set_hypervolume_reference(&mut self, reference: Vec<T>) -> Result<()> {
if reference.len() != self.config.objectives.len() {
return Err(OptimError::InvalidConfig(format!(
"hypervolume reference has {} entries but {} objectives are configured",
reference.len(),
self.config.objectives.len()
)));
}
self.hypervolume_reference = Some(reference);
Ok(())
}
fn build_decomposition(&mut self, config: &MultiObjectiveConfig<T>) -> Result<()> {
let num_objectives = config.objectives.len();
if num_objectives == 0 {
return Err(OptimError::InvalidConfig(
"MOEA/D needs at least one configured objective to decompose".to_string(),
));
}
let (divisions, lattice_size) =
divisions_for_at_least(num_objectives, self.subproblem_target).ok_or_else(|| {
OptimError::InvalidConfig(format!(
"no Das-Dennis lattice with at least {} vectors exists for {} objectives",
self.subproblem_target, num_objectives
))
})?;
self.divisions = divisions;
self.weight_vectors = das_dennis_weights::<T>(num_objectives, divisions);
if self.weight_vectors.len() != lattice_size {
return Err(OptimError::OptimizationError(format!(
"weight lattice size {} disagrees with the computed {}",
self.weight_vectors.len(),
lattice_size
)));
}
self.neighbors = weight_neighborhoods(&self.weight_vectors, self.neighborhood_size);
self.ideal_point = vec![T::infinity(); num_objectives];
self.population.clear();
self.subproblem_of.clear();
self.subproblem_order.clear();
for index in 0..self.weight_vectors.len() {
let architecture = operators::sample_architecture::<T>(&mut self.rng, "arch");
let id = architecture.architecture_id.clone();
self.population.push(Individual {
architecture,
objectives: Vec::new(),
constraints: Vec::new(),
rank: 0,
crowding_distance: T::zero(),
fitness: T::zero(),
id: id.clone(),
});
self.remember_subproblem(id, index);
}
Ok(())
}
fn remember_subproblem(&mut self, architecture_id: String, index: usize) {
if architecture_id.is_empty() {
return;
}
let capacity = (self.weight_vectors.len().max(1)).saturating_mul(4);
if self
.subproblem_of
.insert(architecture_id.clone(), index)
.is_none()
{
self.subproblem_order.push_back(architecture_id);
}
while self.subproblem_order.len() > capacity {
if let Some(oldest) = self.subproblem_order.pop_front() {
self.subproblem_of.remove(&oldest);
}
}
}
fn minimization_objectives(&self, result: &SearchResult<T>) -> Vec<T> {
let raw = objective_vector_for_result(&self.config.objectives, result);
self.to_minimization(&raw)
}
fn to_minimization(&self, raw: &[T]) -> Vec<T> {
let directions = objective_directions(&self.config.objectives);
normalize_front_for_minimization(&[raw.to_vec()], &directions)
.into_iter()
.next()
.unwrap_or_default()
}
fn effective_ideal(&self, reference: &[Vec<T>]) -> Vec<T> {
let num_objectives = self.config.objectives.len();
let mut ideal = self.ideal_point.clone();
if ideal.len() < num_objectives {
ideal.resize(num_objectives, T::infinity());
}
for point in reference {
update_ideal_point(&mut ideal, point);
}
for value in ideal.iter_mut() {
if !value.is_finite() {
*value = T::zero();
}
}
ideal
}
fn subproblem_cost(&self, index: usize, minimized: &[T], ideal: &[T]) -> Option<T> {
let weight = self.weight_vectors.get(index)?;
Some(scalarize(
self.decomposition.scalarization(),
minimized,
weight,
ideal,
))
}
fn best_fit_subproblem(&self, minimized: &[T], ideal: &[T]) -> Option<usize> {
let mut best_index = None;
let mut best_cost = T::infinity();
for index in 0..self.weight_vectors.len() {
let Some(cost) = self.subproblem_cost(index, minimized, ideal) else {
continue;
};
if cost < best_cost {
best_cost = cost;
best_index = Some(index);
}
}
best_index
}
fn is_evaluated(&self, individual: &Individual<T>) -> bool {
!self.config.objectives.is_empty()
&& individual.objectives.len() == self.config.objectives.len()
}
fn dominates(a: &[T], b: &[T]) -> bool {
if a.len() != b.len() || a.is_empty() {
return false;
}
let mut strictly_better = false;
for (left, right) in a.iter().zip(b.iter()) {
if *left > *right {
return false;
}
if *left < *right {
strictly_better = true;
}
}
strictly_better
}
fn offer_to_archive(&mut self, candidate: &Individual<T>, minimized: &[T]) -> bool {
if minimized.is_empty() {
return false;
}
let member_objectives: Vec<Vec<T>> = self
.archive
.iter()
.map(|member| self.to_minimization(&member.objectives))
.collect();
let already_covered = member_objectives
.iter()
.any(|member| Self::dominates(member, minimized) || member.as_slice() == minimized);
if already_covered {
return false;
}
let candidate_id = candidate.architecture.architecture_id.as_str();
let mut index = 0usize;
self.archive.retain(|member| {
let dominated = Self::dominates(minimized, &member_objectives[index]);
let superseded =
!candidate_id.is_empty() && member.architecture.architecture_id == candidate_id;
index += 1;
!dominated && !superseded
});
self.archive.push(candidate.clone());
self.truncate_archive();
true
}
fn truncate_archive(&mut self) {
while self.archive.len() > self.archive_capacity {
let points: Vec<Vec<T>> = self
.archive
.iter()
.map(|member| self.to_minimization(&member.objectives))
.collect();
let Some(victim) = most_crowded_index(&points) else {
break;
};
self.archive.remove(victim);
}
}
fn publish_archive(&mut self) {
let solutions: Vec<ParetoSolution<T>> = self
.archive
.iter()
.map(|member| ParetoSolution {
architecture: member.architecture.clone(),
objectives: member.objectives.clone(),
constraint_violations: member.constraints.clone(),
rank: 0,
crowding_distance: member.crowding_distance,
metadata: SolutionMetadata {
id: member.id.clone(),
generation: self.generation,
evaluation_count: self.statistics.total_evaluations,
parents: Vec::new(),
creation_method: creation_method_from_id(&member.architecture.architecture_id),
},
})
.collect();
self.pareto_front.solutions = solutions;
self.pareto_front.generation = self.generation;
self.pareto_front.last_updated = std::time::SystemTime::now();
self.update_objective_bounds();
self.update_front_metrics();
}
fn update_objective_bounds(&mut self) {
let Some(first) = self.pareto_front.solutions.first() else {
return;
};
let num_objectives = first.objectives.len();
let mut min_values = vec![T::infinity(); num_objectives];
let mut max_values = vec![T::neg_infinity(); num_objectives];
for solution in &self.pareto_front.solutions {
for (index, value) in solution.objectives.iter().enumerate() {
if index >= num_objectives {
break;
}
if *value < min_values[index] {
min_values[index] = *value;
}
if *value > max_values[index] {
max_values[index] = *value;
}
}
}
self.pareto_front.objective_bounds = ObjectiveBounds {
min_values: min_values.clone(),
max_values: max_values.clone(),
ideal_point: min_values,
nadir_point: max_values,
};
}
fn front_in_minimization_space(&self) -> Vec<Vec<T>> {
let directions = objective_directions(&self.config.objectives);
let raw: Vec<Vec<T>> = self
.pareto_front
.solutions
.iter()
.map(|solution| solution.objectives.clone())
.collect();
normalize_front_for_minimization(&raw, &directions)
}
fn calculate_hypervolume(&mut self) -> T {
let front = self.front_in_minimization_space();
if front.is_empty() {
return T::zero();
}
let directions = objective_directions(&self.config.objectives);
if self.hypervolume_reference.is_none() {
let Some(derived) = derive_reference_point(&front) else {
return T::zero();
};
self.hypervolume_reference =
Some(normalize_reference_for_minimization(&derived, &directions));
}
let Some(raw_reference) = self.hypervolume_reference.as_ref() else {
return T::zero();
};
let reference = normalize_reference_for_minimization(raw_reference, &directions);
hypervolume_minimization(&front, &reference)
}
fn update_front_metrics(&mut self) {
let front = self.front_in_minimization_space();
let hypervolume = self.calculate_hypervolume();
let previous = self.previous_hypervolume;
self.previous_hypervolume = Some(hypervolume);
let directions = objective_directions(&self.config.objectives);
let reference = self
.hypervolume_reference
.as_ref()
.map(|raw| normalize_reference_for_minimization(raw, &directions))
.unwrap_or_default();
let population: Vec<Vec<T>> = normalize_front_for_minimization(
&self
.population
.iter()
.map(|individual| individual.objectives.clone())
.collect::<Vec<_>>(),
&directions,
);
self.pareto_front.metrics = metrics::front_metrics_in_minimization_space(
&front,
&population,
&reference,
hypervolume,
previous,
);
self.statistics.pareto_front_size = self.pareto_front.solutions.len();
if hypervolume > self.statistics.best_hypervolume {
self.statistics.best_hypervolume = hypervolume;
}
self.statistics.generation = self.generation;
self.statistics
.convergence_history
.push(self.pareto_front.metrics.convergence);
self.statistics
.diversity_history
.push(self.pareto_front.metrics.spread);
let solved = self
.population
.iter()
.filter(|individual| self.is_evaluated(individual))
.count();
self.statistics.algorithm_metrics.insert(
"subproblems".to_string(),
T::from(self.weight_vectors.len()).unwrap_or_else(T::zero),
);
self.statistics.algorithm_metrics.insert(
"solved_subproblems".to_string(),
T::from(solved).unwrap_or_else(T::zero),
);
self.statistics.algorithm_metrics.insert(
"archive_size".to_string(),
T::from(self.archive.len()).unwrap_or_else(T::zero),
);
self.statistics.algorithm_metrics.insert(
"replacements".to_string(),
T::from(self.last_replacements).unwrap_or_else(T::zero),
);
}
pub fn last_replacements(&self) -> usize {
self.last_replacements
}
pub fn archive(&self) -> &[Individual<T>] {
&self.archive
}
pub(crate) fn select_by_decomposition(
&self,
results: &[SearchResult<T>],
k: usize,
) -> Vec<usize> {
if results.is_empty() || k == 0 || self.weight_vectors.is_empty() {
return Vec::new();
}
let minimized_results: Vec<Vec<T>> = results
.iter()
.map(|result| self.minimization_objectives(result))
.collect();
let ideal = self.effective_ideal(&minimized_results);
let mut scored: Vec<(usize, usize, T)> = Vec::with_capacity(results.len());
for (index, minimized) in minimized_results.iter().enumerate() {
let Some(subproblem) = self.best_fit_subproblem(minimized, &ideal) else {
continue;
};
let cost = self
.subproblem_cost(subproblem, minimized, &ideal)
.unwrap_or_else(T::infinity);
scored.push((index, subproblem, cost));
}
scored.sort_by(|a, b| {
a.2.partial_cmp(&b.2)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
let mut taken_subproblems = std::collections::HashSet::new();
let mut selected = Vec::with_capacity(k.min(scored.len()));
for (index, subproblem, _) in &scored {
if selected.len() >= k {
break;
}
if taken_subproblems.insert(*subproblem) {
selected.push(*index);
}
}
for (index, _, _) in &scored {
if selected.len() >= k {
break;
}
if !selected.contains(index) {
selected.push(*index);
}
}
selected
}
pub(crate) fn mean_objective_distance(&self, results: &[SearchResult<T>]) -> f64 {
let vectors: Vec<Vec<T>> = results
.iter()
.map(|result| objective_vector_for_result(&self.config.objectives, result))
.collect();
metrics::mean_pairwise_distance(&vectors)
}
}
fn most_crowded_index<T: Float>(points: &[Vec<T>]) -> Option<usize> {
if points.len() < 2 {
return None;
}
let dims = points.iter().map(|point| point.len()).min()?;
if dims == 0 {
return None;
}
let mut min_values = vec![T::infinity(); dims];
let mut max_values = vec![T::neg_infinity(); dims];
for point in points {
for index in 0..dims {
if point[index] < min_values[index] {
min_values[index] = point[index];
}
if point[index] > max_values[index] {
max_values[index] = point[index];
}
}
}
let spans: Vec<T> = (0..dims)
.map(|index| {
let span = max_values[index] - min_values[index];
if span > T::zero() {
span
} else {
T::one()
}
})
.collect();
let sorted_distances = |i: usize| -> Vec<T> {
let point = &points[i];
let mut distances: Vec<T> = points
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, other)| {
let mut sum = T::zero();
for index in 0..dims {
let diff = (point[index] - other[index]) / spans[index];
sum = sum + diff * diff;
}
sum.sqrt()
})
.collect();
distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
distances
};
let mut worst_index = 0usize;
let mut worst_profile = sorted_distances(0);
for i in 1..points.len() {
let profile = sorted_distances(i);
let mut smaller = false;
for (candidate, incumbent) in profile.iter().zip(worst_profile.iter()) {
if *candidate < *incumbent {
smaller = true;
break;
}
if *candidate > *incumbent {
break;
}
}
if smaller {
worst_index = i;
worst_profile = profile;
}
}
Some(worst_index)
}
fn creation_method_from_id(architecture_id: &str) -> CreationMethod {
if architecture_id.starts_with("offspring_") {
CreationMethod::Crossover
} else if architecture_id.starts_with("mutant_") {
CreationMethod::Mutation
} else if architecture_id.starts_with("arch_") {
CreationMethod::RandomGeneration
} else {
CreationMethod::Custom
}
}
impl<
T: Float
+ Debug
+ Default
+ Clone
+ Send
+ Sync
+ std::fmt::Debug
+ PartialOrd
+ std::iter::Sum,
> MultiObjectiveOptimizer<T> for MOEADOptimizer<T>
{
fn initialize(&mut self, config: &MultiObjectiveConfig<T>) -> Result<()> {
self.config = config.clone();
self.generation = 0;
self.statistics = MultiObjectiveStatistics::default();
self.archive.clear();
self.pareto_front = ParetoFront::default();
self.hypervolume_reference = None;
self.previous_hypervolume = None;
self.last_replacements = 0;
self.build_decomposition(config)
}
fn update_pareto_front(&mut self, results: &[SearchResult<T>]) -> Result<ParetoFront<T>> {
if self.weight_vectors.is_empty() {
return Err(OptimError::InvalidConfig(
"MOEA/D has no weight vectors; call initialize with a configuration \
that declares at least one objective first"
.to_string(),
));
}
self.last_replacements = 0;
for result in results {
let raw = objective_vector_for_result(&self.config.objectives, result);
let minimized = self.to_minimization(&raw);
if minimized.len() != self.config.objectives.len() {
continue;
}
update_ideal_point(&mut self.ideal_point, &minimized);
let ideal = self.effective_ideal(&[]);
let architecture_id = result.architecture.architecture_id.clone();
let owner = self
.subproblem_of
.get(&architecture_id)
.copied()
.or_else(|| self.best_fit_subproblem(&minimized, &ideal));
let Some(owner) = owner else {
continue;
};
self.subproblem_of.remove(&architecture_id);
let candidate = Individual {
architecture: result.architecture.clone(),
objectives: raw,
constraints: Vec::new(),
rank: 0,
crowding_distance: T::zero(),
fitness: T::zero(),
id: if architecture_id.is_empty() {
format!("moead_{}_{}", self.generation, owner)
} else {
architecture_id
},
};
let owner_holds_candidate = self.population.get(owner).is_some_and(|incumbent| {
!candidate.architecture.architecture_id.is_empty()
&& incumbent.architecture.architecture_id
== candidate.architecture.architecture_id
&& !self.is_evaluated(incumbent)
});
if owner_holds_candidate {
self.population[owner] = candidate.clone();
self.offer_to_archive(&candidate, &minimized);
continue;
}
let mut neighborhood = self
.neighbors
.get(owner)
.cloned()
.unwrap_or_else(|| vec![owner]);
for position in (1..neighborhood.len()).rev() {
let other = self.rng.gen_range(0..=position);
neighborhood.swap(position, other);
}
let mut replacements = 0usize;
for neighbor in neighborhood {
if replacements >= MAX_REPLACEMENTS {
break;
}
let Some(incumbent) = self.population.get(neighbor) else {
continue;
};
let accept = if self.is_evaluated(incumbent) {
let incumbent_min = self.to_minimization(&incumbent.objectives);
match (
self.subproblem_cost(neighbor, &minimized, &ideal),
self.subproblem_cost(neighbor, &incumbent_min, &ideal),
) {
(Some(new_cost), Some(old_cost)) => new_cost < old_cost,
_ => false,
}
} else {
true
};
if accept {
self.population[neighbor] = candidate.clone();
replacements += 1;
}
}
self.last_replacements += replacements;
self.offer_to_archive(&candidate, &minimized);
}
self.generation += 1;
self.statistics.total_evaluations += results.len();
self.publish_archive();
Ok(self.pareto_front.clone())
}
fn get_pareto_front(&self) -> &ParetoFront<T> {
&self.pareto_front
}
fn select_candidates(
&mut self,
population: &[OptimizerArchitecture<T>],
_objectives: &[T],
) -> Result<Vec<OptimizerArchitecture<T>>> {
if self.weight_vectors.is_empty() {
return Err(OptimError::InvalidConfig(
"MOEA/D has no weight vectors; call initialize with a configuration \
that declares at least one objective first"
.to_string(),
));
}
let subproblems = self.weight_vectors.len();
let mut offspring = Vec::with_capacity(subproblems);
for index in 0..subproblems {
let use_neighborhood = self.rng.gen_range(0.0..1.0) < NEIGHBORHOOD_SELECTION_PROB;
let pool: Vec<usize> = if use_neighborhood {
self.neighbors
.get(index)
.cloned()
.unwrap_or_else(|| vec![index])
} else {
(0..self.population.len()).collect()
};
if pool.is_empty() {
continue;
}
let first = pool[self.rng.gen_range(0..pool.len())];
let second = pool[self.rng.gen_range(0..pool.len())];
let parent1 = self
.population
.get(first)
.map(|individual| individual.architecture.clone());
let parent2 = if !population.is_empty() && !use_neighborhood {
Some(population[self.rng.gen_range(0..population.len())].clone())
} else {
self.population
.get(second)
.map(|individual| individual.architecture.clone())
};
let (Some(parent1), Some(parent2)) = (parent1, parent2) else {
continue;
};
let mut child = if self.rng.gen_range(0.0..1.0) < MOEAD_CROSSOVER_PROB {
operators::crossover_architectures(&mut self.rng, &parent1, &parent2)
} else {
parent1
};
if self.rng.gen_range(0.0..1.0) < MOEAD_MUTATION_PROB {
operators::mutate_architecture(
&mut self.rng,
&mut child,
MOEAD_COMPONENT_MUTATION_RATE,
MOEAD_PARAMETER_MUTATION_RATE,
);
}
self.remember_subproblem(child.architecture_id.clone(), index);
offspring.push(child);
}
Ok(offspring)
}
fn name(&self) -> &str {
"MOEA/D"
}
fn get_statistics(&self) -> MultiObjectiveStatistics<T> {
self.statistics.clone()
}
}