optirs_nas/multi_objective/
nsga3.rs1use crate::error::{OptimError, Result};
30use crate::nas_engine::{MultiObjectiveConfig, OptimizerArchitecture, SearchResult};
31use scirs2_core::numeric::Float;
32use std::fmt::Debug;
33
34use super::core::{
35 objective_directions, Individual, MultiObjectiveOptimizer, MultiObjectiveStatistics,
36 ParetoFront,
37};
38use super::decomposition::{
39 associate_with_references, das_dennis_weights, divisions_for_at_least, normalize_by_hyperplane,
40};
41use super::hypervolume::normalize_front_for_minimization;
42use super::nsga2::NSGA2;
43use super::operators;
44
45pub const DEFAULT_REFERENCE_TARGET: usize = 20;
48
49const NSGA3_COMPONENT_MUTATION_RATE: f64 = 0.2;
51
52const NSGA3_PARAMETER_MUTATION_RATE: f64 = 0.3;
54
55pub struct NSGA3<T: Float + Debug + Send + Sync + 'static> {
57 pub(super) base: NSGA2<T>,
60 pub(super) reference_directions: Vec<Vec<T>>,
62 pub(super) association_count: Vec<usize>,
65 pub(super) niche_count: Vec<usize>,
68 pub(super) reference_target: usize,
70 pub(super) divisions: usize,
72}
73
74impl<
75 T: Float
76 + Debug
77 + Default
78 + Clone
79 + Send
80 + Sync
81 + std::fmt::Debug
82 + PartialOrd
83 + std::iter::Sum,
84 > NSGA3<T>
85{
86 pub fn new(reference_target: usize, crossover_prob: f64, mutation_prob: f64) -> Self {
88 Self::with_seed(
89 reference_target,
90 crossover_prob,
91 mutation_prob,
92 scirs2_core::random::random::<u64>(),
93 )
94 }
95
96 pub fn with_seed(
98 reference_target: usize,
99 crossover_prob: f64,
100 mutation_prob: f64,
101 seed: u64,
102 ) -> Self {
103 let reference_target = reference_target.max(1);
104 Self {
105 base: NSGA2::with_seed(reference_target, crossover_prob, mutation_prob, seed),
106 reference_directions: Vec::new(),
107 association_count: Vec::new(),
108 niche_count: Vec::new(),
109 reference_target,
110 divisions: 0,
111 }
112 }
113
114 pub fn reference_directions(&self) -> &[Vec<T>] {
116 &self.reference_directions
117 }
118
119 pub fn association_count(&self) -> &[usize] {
122 &self.association_count
123 }
124
125 pub fn niche_count(&self) -> &[usize] {
128 &self.niche_count
129 }
130
131 pub fn population_size(&self) -> usize {
133 self.base.population_size
134 }
135
136 pub fn set_hypervolume_reference(&mut self, reference: Vec<T>) -> Result<()> {
138 self.base.set_hypervolume_reference(reference)
139 }
140
141 fn build_reference_directions(&mut self, config: &MultiObjectiveConfig<T>) -> Result<()> {
143 let num_objectives = config.objectives.len();
144 if num_objectives == 0 {
145 return Err(OptimError::InvalidConfig(
146 "NSGA-III needs at least one configured objective to place reference \
147 directions"
148 .to_string(),
149 ));
150 }
151 let (divisions, lattice_size) =
152 divisions_for_at_least(num_objectives, self.reference_target).ok_or_else(|| {
153 OptimError::InvalidConfig(format!(
154 "no Das-Dennis lattice with at least {} directions exists for {} \
155 objectives",
156 self.reference_target, num_objectives
157 ))
158 })?;
159 self.divisions = divisions;
160 self.reference_directions = das_dennis_weights::<T>(num_objectives, divisions);
161 self.association_count = vec![0; self.reference_directions.len()];
162 self.niche_count = vec![0; self.reference_directions.len()];
163 self.base.population_size = lattice_size;
166 Ok(())
167 }
168
169 fn minimization_objectives(&self, individuals: &[Individual<T>]) -> Vec<Vec<T>> {
171 let directions = objective_directions(&self.base.config.objectives);
172 let raw: Vec<Vec<T>> = individuals
173 .iter()
174 .map(|individual| individual.objectives.clone())
175 .collect();
176 normalize_front_for_minimization(&raw, &directions)
177 }
178
179 fn niching_selection(
187 &mut self,
188 already_selected: &[usize],
189 splitting_front: &[usize],
190 slots: usize,
191 ) -> Vec<usize> {
192 if slots == 0 || splitting_front.is_empty() || self.reference_directions.is_empty() {
193 return Vec::new();
194 }
195 let considered: Vec<usize> = already_selected
198 .iter()
199 .chain(splitting_front.iter())
200 .copied()
201 .collect();
202 let individuals: Vec<Individual<T>> = considered
203 .iter()
204 .filter_map(|index| self.base.population.get(*index).cloned())
205 .collect();
206 if individuals.len() != considered.len() {
207 return splitting_front.iter().copied().take(slots).collect();
210 }
211 let points = self.minimization_objectives(&individuals);
212 let num_objectives = self.base.config.objectives.len();
213 let Some((normalized, _intercepts)) = normalize_by_hyperplane(&points, num_objectives)
214 else {
215 return splitting_front.iter().copied().take(slots).collect();
216 };
217 let associations = associate_with_references(&normalized, &self.reference_directions);
218
219 let mut niche_count = vec![0usize; self.reference_directions.len()];
221 let mut association_count = vec![0usize; self.reference_directions.len()];
222 for (position, (direction, _)) in associations.iter().enumerate() {
223 if *direction < association_count.len() {
224 association_count[*direction] += 1;
225 if position < already_selected.len() {
226 niche_count[*direction] += 1;
227 }
228 }
229 }
230
231 let offset = already_selected.len();
234 let mut available: Vec<Vec<(usize, T)>> = vec![Vec::new(); self.reference_directions.len()];
235 for (position, (direction, distance)) in associations.iter().enumerate().skip(offset) {
236 let candidate = considered[position];
237 if *direction < available.len() {
238 available[*direction].push((candidate, *distance));
239 }
240 }
241
242 let mut excluded = vec![false; self.reference_directions.len()];
243 let mut chosen = Vec::with_capacity(slots);
244 while chosen.len() < slots {
245 let mut best_directions: Vec<usize> = Vec::new();
247 let mut best_count = usize::MAX;
248 for direction in 0..self.reference_directions.len() {
249 if excluded[direction] || available[direction].is_empty() {
250 continue;
251 }
252 let count = niche_count[direction];
253 if count < best_count {
254 best_count = count;
255 best_directions.clear();
256 best_directions.push(direction);
257 } else if count == best_count {
258 best_directions.push(direction);
259 }
260 }
261 if best_directions.is_empty() {
262 break;
263 }
264 let picked = best_directions[self.base.rng.gen_range(0..best_directions.len())];
267
268 let position = if niche_count[picked] == 0 {
269 let mut best_position = 0usize;
271 let mut best_distance = T::infinity();
272 for (index, (_, distance)) in available[picked].iter().enumerate() {
273 if *distance < best_distance {
274 best_distance = *distance;
275 best_position = index;
276 }
277 }
278 best_position
279 } else {
280 self.base.rng.gen_range(0..available[picked].len())
283 };
284 let (candidate, _) = available[picked].remove(position);
285 chosen.push(candidate);
286 niche_count[picked] += 1;
287 if available[picked].is_empty() {
288 excluded[picked] = true;
289 }
290 }
291
292 self.niche_count = niche_count;
293 self.association_count = association_count;
294 chosen
295 }
296
297 fn environmental_selection(&mut self) -> Result<()> {
300 let target = self.base.population_size.max(1);
301 let fronts = self.base.non_dominated_sort();
302 let mut selected: Vec<usize> = Vec::with_capacity(target);
303 let mut splitting: Vec<usize> = Vec::new();
304 for front in &fronts {
305 let evaluated: Vec<usize> = front
306 .iter()
307 .copied()
308 .filter(|index| {
309 self.base
310 .population
311 .get(*index)
312 .is_some_and(|individual| self.base.is_evaluated(individual))
313 })
314 .collect();
315 if evaluated.is_empty() {
316 continue;
317 }
318 if selected.len() + evaluated.len() <= target {
319 selected.extend(evaluated);
320 } else {
321 splitting = evaluated;
322 break;
323 }
324 }
325 if !splitting.is_empty() && selected.len() < target {
326 let slots = target - selected.len();
327 let chosen = self.niching_selection(&selected, &splitting, slots);
328 selected.extend(chosen);
329 } else if splitting.is_empty() {
330 self.refresh_association_counts(&selected);
334 }
335
336 let unevaluated: Vec<usize> = (0..self.base.population.len())
339 .filter(|index| {
340 self.base
341 .population
342 .get(*index)
343 .is_some_and(|individual| !self.base.is_evaluated(individual))
344 })
345 .collect();
346 let keep_unevaluated = target.saturating_sub(selected.len());
347
348 let mut kept: Vec<Individual<T>> = selected
349 .iter()
350 .filter_map(|index| self.base.population.get(*index).cloned())
351 .collect();
352 kept.extend(
353 unevaluated
354 .iter()
355 .take(keep_unevaluated)
356 .filter_map(|index| self.base.population.get(*index).cloned()),
357 );
358 self.base.population = kept;
359 Ok(())
360 }
361
362 fn refresh_association_counts(&mut self, selected: &[usize]) {
365 let mut niche_count = vec![0usize; self.reference_directions.len()];
366 let mut association_count = vec![0usize; self.reference_directions.len()];
367 let individuals: Vec<Individual<T>> = selected
368 .iter()
369 .filter_map(|index| self.base.population.get(*index).cloned())
370 .collect();
371 if !individuals.is_empty() && !self.reference_directions.is_empty() {
372 let points = self.minimization_objectives(&individuals);
373 let num_objectives = self.base.config.objectives.len();
374 if let Some((normalized, _)) = normalize_by_hyperplane(&points, num_objectives) {
375 for (direction, _) in
376 associate_with_references(&normalized, &self.reference_directions)
377 {
378 if direction < niche_count.len() {
379 niche_count[direction] += 1;
380 association_count[direction] += 1;
381 }
382 }
383 }
384 }
385 self.niche_count = niche_count;
386 self.association_count = association_count;
387 }
388
389 fn absorb_results(&mut self, results: &[SearchResult<T>]) {
391 for result in results {
392 let objectives = self.base.objective_vector_for_result(result);
393 let architecture_id = result.architecture.architecture_id.as_str();
394 let existing = if architecture_id.is_empty() {
395 None
396 } else {
397 self.base.population.iter().position(|individual| {
398 individual.architecture.architecture_id == architecture_id
399 })
400 };
401 match existing {
402 Some(index) => {
403 self.base.population[index].objectives = objectives;
404 self.base.population[index].architecture = result.architecture.clone();
405 }
406 None => {
407 let id = if architecture_id.is_empty() {
408 format!("nsga3_{}", self.base.population.len())
409 } else {
410 architecture_id.to_string()
411 };
412 self.base.population.push(Individual {
413 id,
414 architecture: result.architecture.clone(),
415 objectives,
416 constraints: Vec::new(),
417 rank: 0,
418 crowding_distance: T::zero(),
419 fitness: T::zero(),
420 });
421 }
422 }
423 }
424 }
425
426 pub(crate) fn mean_objective_distance(&self, results: &[SearchResult<T>]) -> f64 {
429 self.base.mean_objective_distance(results)
430 }
431
432 pub(crate) fn select_by_rank_and_niching(
436 &mut self,
437 results: &[SearchResult<T>],
438 k: usize,
439 ) -> Vec<usize> {
440 if results.is_empty() || k == 0 {
441 return Vec::new();
442 }
443 let saved_population = std::mem::take(&mut self.base.population);
446 let saved_size = self.base.population_size;
447 self.base.population = results
448 .iter()
449 .enumerate()
450 .map(|(index, result)| Individual {
451 architecture: result.architecture.clone(),
452 objectives: self.base.objective_vector_for_result(result),
453 constraints: Vec::new(),
454 rank: 0,
455 crowding_distance: T::zero(),
456 fitness: T::zero(),
457 id: format!("sel_{}", index),
458 })
459 .collect();
460 self.base.population_size = k.min(results.len());
461
462 let fronts = self.base.non_dominated_sort();
463 let mut selected: Vec<usize> = Vec::with_capacity(self.base.population_size);
464 let mut splitting: Vec<usize> = Vec::new();
465 for front in &fronts {
466 if selected.len() + front.len() <= self.base.population_size {
467 selected.extend(front.iter().copied());
468 } else {
469 splitting = front.clone();
470 break;
471 }
472 }
473 if !splitting.is_empty() && selected.len() < self.base.population_size {
474 let slots = self.base.population_size - selected.len();
475 let chosen = self.niching_selection(&selected, &splitting, slots);
476 selected.extend(chosen);
477 }
478
479 self.base.population = saved_population;
480 self.base.population_size = saved_size;
481 selected
482 }
483}
484
485impl<
486 T: Float
487 + Debug
488 + Default
489 + Clone
490 + Send
491 + Sync
492 + std::fmt::Debug
493 + PartialOrd
494 + std::iter::Sum,
495 > MultiObjectiveOptimizer<T> for NSGA3<T>
496{
497 fn initialize(&mut self, config: &MultiObjectiveConfig<T>) -> Result<()> {
498 self.build_reference_directions(config)?;
499 self.base.initialize(config)
502 }
503
504 fn update_pareto_front(&mut self, results: &[SearchResult<T>]) -> Result<ParetoFront<T>> {
507 if self.reference_directions.is_empty() {
508 return Err(OptimError::InvalidConfig(
509 "NSGA-III has no reference directions; call initialize with a \
510 configuration that declares at least one objective first"
511 .to_string(),
512 ));
513 }
514 self.absorb_results(results);
515 self.base.generation += 1;
516 self.base.statistics.total_evaluations += results.len();
517 self.environmental_selection()?;
518 let fronts = self.base.non_dominated_sort();
520 for front in &fronts {
521 self.base.calculate_crowding_distance(front);
522 }
523 self.base.update_pareto_front_from_population();
524 let niched = self.niche_count.iter().filter(|count| **count > 0).count();
525 self.base.statistics.algorithm_metrics.insert(
526 "reference_directions".to_string(),
527 T::from(self.reference_directions.len()).unwrap_or_else(T::zero),
528 );
529 self.base.statistics.algorithm_metrics.insert(
530 "occupied_niches".to_string(),
531 T::from(niched).unwrap_or_else(T::zero),
532 );
533 Ok(self.base.get_pareto_front().clone())
534 }
535
536 fn get_pareto_front(&self) -> &ParetoFront<T> {
537 self.base.get_pareto_front()
538 }
539
540 fn select_candidates(
546 &mut self,
547 population: &[OptimizerArchitecture<T>],
548 _objectives: &[T],
549 ) -> Result<Vec<OptimizerArchitecture<T>>> {
550 if self.base.population.is_empty() && population.is_empty() {
551 return Err(OptimError::InvalidConfig(
552 "NSGA-III has no population to reproduce from; call initialize first".to_string(),
553 ));
554 }
555 let pool: Vec<OptimizerArchitecture<T>> = if self.base.population.is_empty() {
556 population.to_vec()
557 } else {
558 self.base
559 .population
560 .iter()
561 .map(|individual| individual.architecture.clone())
562 .chain(population.iter().cloned())
563 .collect()
564 };
565 let mut offspring = Vec::with_capacity(self.base.population_size);
566 for _ in 0..self.base.population_size {
567 let first = pool[self.base.rng.gen_range(0..pool.len())].clone();
568 let second = pool[self.base.rng.gen_range(0..pool.len())].clone();
569 let mut child = if self.base.rng.gen_range(0.0..1.0) < self.base.crossover_prob {
570 operators::crossover_architectures(&mut self.base.rng, &first, &second)
571 } else {
572 first
573 };
574 if self.base.rng.gen_range(0.0..1.0) < self.base.mutation_prob {
575 operators::mutate_architecture(
576 &mut self.base.rng,
577 &mut child,
578 NSGA3_COMPONENT_MUTATION_RATE,
579 NSGA3_PARAMETER_MUTATION_RATE,
580 );
581 }
582 offspring.push(child);
583 }
584 Ok(offspring)
585 }
586
587 fn name(&self) -> &str {
588 "NSGA-III"
589 }
590
591 fn get_statistics(&self) -> MultiObjectiveStatistics<T> {
592 self.base.get_statistics()
593 }
594}