1use crate::error::OptimizeError;
7use ndarray::{Array1, Array2, ArrayView1};
8use rand::{prelude::*, rng};
9use std::cmp::Ordering;
10use std::collections::HashMap;
11
12#[derive(Debug, Clone)]
14pub struct MultiObjectiveSolution {
15 pub variables: Array1<f64>,
17 pub objectives: Array1<f64>,
19 pub constraint_violation: f64,
21 pub rank: usize,
23 pub crowding_distance: f64,
25 pub metadata: HashMap<String, f64>,
27}
28
29#[derive(Debug, Clone)]
31pub struct MultiObjectiveResult {
32 pub pareto_front: Vec<MultiObjectiveSolution>,
34 pub population: Vec<MultiObjectiveSolution>,
36 pub n_evaluations: usize,
38 pub n_generations: usize,
40 pub success: bool,
42 pub message: String,
44 pub hypervolume: Option<f64>,
46}
47
48#[derive(Debug, Clone)]
50pub struct MultiObjectiveConfig {
51 pub population_size: usize,
53 pub max_generations: usize,
55 pub max_evaluations: Option<usize>,
57 pub crossover_probability: f64,
59 pub mutation_probability: f64,
61 pub mutation_eta: f64,
63 pub crossover_eta: f64,
65 pub tolerance: f64,
67 pub reference_point: Option<Array1<f64>>,
69 pub bounds: Option<(Array1<f64>, Array1<f64>)>,
71}
72
73impl Default for MultiObjectiveConfig {
74 fn default() -> Self {
75 Self {
76 population_size: 100,
77 max_generations: 250,
78 max_evaluations: None,
79 crossover_probability: 0.9,
80 mutation_probability: 0.1,
81 mutation_eta: 20.0,
82 crossover_eta: 15.0,
83 tolerance: 1e-6,
84 reference_point: None,
85 bounds: None,
86 }
87 }
88}
89
90pub struct NSGAII {
92 config: MultiObjectiveConfig,
93 n_objectives: usize,
94 n_variables: usize,
95 population: Vec<MultiObjectiveSolution>,
96 generation: usize,
97 n_evaluations: usize,
98}
99
100pub struct NSGAIII {
103 config: MultiObjectiveConfig,
104 n_objectives: usize,
105 n_variables: usize,
106 population: Vec<MultiObjectiveSolution>,
107 reference_points: Array2<f64>,
108 generation: usize,
109 n_evaluations: usize,
110}
111
112impl NSGAII {
113 pub fn new(
115 n_variables: usize,
116 n_objectives: usize,
117 config: Option<MultiObjectiveConfig>,
118 ) -> Self {
119 let config = config.unwrap_or_default();
120
121 Self {
122 config,
123 n_objectives,
124 n_variables,
125 population: Vec::new(),
126 generation: 0,
127 n_evaluations: 0,
128 }
129 }
130
131 pub fn with_bounds(
133 mut self,
134 lower: Array1<f64>,
135 upper: Array1<f64>,
136 ) -> Result<Self, OptimizeError> {
137 if lower.len() != self.n_variables || upper.len() != self.n_variables {
138 return Err(OptimizeError::ValueError(
139 "Bounds dimensions must match number of variables".to_string(),
140 ));
141 }
142
143 for (&l, &u) in lower.iter().zip(upper.iter()) {
144 if l >= u {
145 return Err(OptimizeError::ValueError(
146 "Lower bounds must be less than upper bounds".to_string(),
147 ));
148 }
149 }
150
151 self.config.bounds = Some((lower, upper));
152 Ok(self)
153 }
154
155 pub fn optimize<F, C>(
157 &mut self,
158 mut objective_fn: F,
159 mut constraint_fn: Option<C>,
160 ) -> Result<MultiObjectiveResult, OptimizeError>
161 where
162 F: FnMut(&ArrayView1<f64>) -> Array1<f64>,
163 C: FnMut(&ArrayView1<f64>) -> f64,
164 {
165 self.initialize_population(&mut objective_fn, constraint_fn.as_mut())?;
167
168 let mut prev_hypervolume = 0.0;
169 let mut stagnation_count = 0;
170
171 for generation in 0..self.config.max_generations {
172 self.generation = generation;
173
174 if let Some(max_evals) = self.config.max_evaluations {
176 if self.n_evaluations >= max_evals {
177 break;
178 }
179 }
180
181 let offspring = self.create_offspring(&mut objective_fn, constraint_fn.as_mut())?;
183
184 let mut combined_population = self.population.clone();
186 combined_population.extend(offspring);
187
188 self.population = self.environmental_selection(combined_population);
190
191 if let Some(ref reference_point) = self.config.reference_point {
193 let current_hypervolume = self.calculate_hypervolume(reference_point)?;
194
195 if (current_hypervolume - prev_hypervolume).abs() < self.config.tolerance {
196 stagnation_count += 1;
197 if stagnation_count >= 10 {
198 break;
199 }
200 } else {
201 stagnation_count = 0;
202 }
203
204 prev_hypervolume = current_hypervolume;
205 }
206 }
207
208 let pareto_front: Vec<MultiObjectiveSolution> = self
210 .population
211 .iter()
212 .filter(|sol| sol.rank == 0)
213 .cloned()
214 .collect();
215
216 let hypervolume = if let Some(ref reference_point) = self.config.reference_point {
217 Some(self.calculate_hypervolume(reference_point)?)
218 } else {
219 None
220 };
221
222 Ok(MultiObjectiveResult {
223 pareto_front,
224 population: self.population.clone(),
225 n_evaluations: self.n_evaluations,
226 n_generations: self.generation + 1,
227 success: true,
228 message: "NSGA-II optimization completed successfully".to_string(),
229 hypervolume,
230 })
231 }
232
233 fn initialize_population<F, C>(
235 &mut self,
236 objective_fn: &mut F,
237 mut constraint_fn: Option<&mut C>,
238 ) -> Result<(), OptimizeError>
239 where
240 F: FnMut(&ArrayView1<f64>) -> Array1<f64>,
241 C: FnMut(&ArrayView1<f64>) -> f64,
242 {
243 self.population.clear();
244 let mut rng = rng();
245
246 for _ in 0..self.config.population_size {
247 let variables = if let Some((ref lower, ref upper)) = self.config.bounds {
248 Array1::from_shape_fn(self.n_variables, |i| {
249 lower[i] + rng.random::<f64>() * (upper[i] - lower[i])
250 })
251 } else {
252 Array1::from_shape_fn(self.n_variables, |_| rng.random::<f64>() * 2.0 - 1.0)
253 };
254
255 let objectives = objective_fn(&variables.view());
256 self.n_evaluations += 1;
257
258 let constraint_violation = if let Some(ref mut constraint_fn) = constraint_fn {
259 constraint_fn(&variables.view()).max(0.0)
260 } else {
261 0.0
262 };
263
264 let solution = MultiObjectiveSolution {
265 variables,
266 objectives,
267 constraint_violation,
268 rank: 0,
269 crowding_distance: 0.0,
270 metadata: HashMap::new(),
271 };
272
273 self.population.push(solution);
274 }
275
276 self.population = self.environmental_selection(self.population.clone());
278
279 Ok(())
280 }
281
282 fn create_offspring<F, C>(
284 &mut self,
285 objective_fn: &mut F,
286 mut constraint_fn: Option<&mut C>,
287 ) -> Result<Vec<MultiObjectiveSolution>, OptimizeError>
288 where
289 F: FnMut(&ArrayView1<f64>) -> Array1<f64>,
290 C: FnMut(&ArrayView1<f64>) -> f64,
291 {
292 let mut offspring = Vec::new();
293 let mut rng = rng();
294
295 while offspring.len() < self.config.population_size {
296 let parent1 = self.tournament_selection(&mut rng);
298 let parent2 = self.tournament_selection(&mut rng);
299
300 let (mut child1_vars, mut child2_vars) = if rng.random::<f64>()
302 < self.config.crossover_probability
303 {
304 self.simulated_binary_crossover(&parent1.variables, &parent2.variables, &mut rng)
305 } else {
306 (parent1.variables.clone(), parent2.variables.clone())
307 };
308
309 if rng.random::<f64>() < self.config.mutation_probability {
311 self.polynomial_mutation(&mut child1_vars, &mut rng);
312 }
313 if rng.random::<f64>() < self.config.mutation_probability {
314 self.polynomial_mutation(&mut child2_vars, &mut rng);
315 }
316
317 if let Some((ref lower, ref upper)) = self.config.bounds {
319 self.apply_bounds(&mut child1_vars, lower, upper);
320 self.apply_bounds(&mut child2_vars, lower, upper);
321 }
322
323 for child_vars in [child1_vars, child2_vars] {
325 if offspring.len() >= self.config.population_size {
326 break;
327 }
328
329 let objectives = objective_fn(&child_vars.view());
330 self.n_evaluations += 1;
331
332 let constraint_violation = if let Some(ref mut constraint_fn) = constraint_fn {
333 constraint_fn(&child_vars.view()).max(0.0)
334 } else {
335 0.0
336 };
337
338 let solution = MultiObjectiveSolution {
339 variables: child_vars,
340 objectives,
341 constraint_violation,
342 rank: 0,
343 crowding_distance: 0.0,
344 metadata: HashMap::new(),
345 };
346
347 offspring.push(solution);
348 }
349 }
350
351 Ok(offspring)
352 }
353
354 fn tournament_selection(&self, rng: &mut impl Rng) -> &MultiObjectiveSolution {
356 let tournament_size = 2;
357 let mut best_idx = rng.random_range(0..self.population.len());
358
359 for _ in 1..tournament_size {
360 let idx = rng.random_range(0..self.population.len());
361 if self.dominates_or_better(&self.population[idx], &self.population[best_idx]) {
362 best_idx = idx;
363 }
364 }
365
366 &self.population[best_idx]
367 }
368
369 fn dominates_or_better(&self, a: &MultiObjectiveSolution, b: &MultiObjectiveSolution) -> bool {
371 if a.constraint_violation < b.constraint_violation {
373 return true;
374 } else if a.constraint_violation > b.constraint_violation {
375 return false;
376 }
377
378 match a.rank.cmp(&b.rank) {
380 std::cmp::Ordering::Less => true,
381 std::cmp::Ordering::Equal => a.crowding_distance > b.crowding_distance,
382 std::cmp::Ordering::Greater => false,
383 }
384 }
385
386 fn simulated_binary_crossover(
388 &self,
389 parent1: &Array1<f64>,
390 parent2: &Array1<f64>,
391 rng: &mut impl Rng,
392 ) -> (Array1<f64>, Array1<f64>) {
393 let mut child1 = parent1.clone();
394 let mut child2 = parent2.clone();
395 let eta = self.config.crossover_eta;
396
397 for i in 0..self.n_variables {
398 if rng.random::<f64>() <= 0.5 {
399 let y1 = parent1[i].min(parent2[i]);
400 let y2 = parent1[i].max(parent2[i]);
401
402 if (y2 - y1).abs() > 1e-14 {
403 let u = rng.random::<f64>();
404 let beta = if u <= 0.5 {
405 (2.0 * u).powf(1.0 / (eta + 1.0))
406 } else {
407 (1.0 / (2.0 * (1.0 - u))).powf(1.0 / (eta + 1.0))
408 };
409
410 child1[i] = 0.5 * ((y1 + y2) - beta * (y2 - y1));
411 child2[i] = 0.5 * ((y1 + y2) + beta * (y2 - y1));
412 }
413 }
414 }
415
416 (child1, child2)
417 }
418
419 fn polynomial_mutation(&self, individual: &mut Array1<f64>, rng: &mut impl Rng) {
421 let eta = self.config.mutation_eta;
422
423 for i in 0..self.n_variables {
424 if rng.random::<f64>() <= 1.0 / self.n_variables as f64 {
425 let u = rng.random::<f64>();
426 let delta = if u < 0.5 {
427 (2.0 * u).powf(1.0 / (eta + 1.0)) - 1.0
428 } else {
429 1.0 - (2.0 * (1.0 - u)).powf(1.0 / (eta + 1.0))
430 };
431
432 if let Some((ref lower, ref upper)) = self.config.bounds {
433 let range = upper[i] - lower[i];
434 individual[i] += delta * range * 0.1; } else {
436 individual[i] += delta * 0.1;
437 }
438 }
439 }
440 }
441
442 fn apply_bounds(&self, individual: &mut Array1<f64>, lower: &Array1<f64>, upper: &Array1<f64>) {
444 for (i, value) in individual.iter_mut().enumerate() {
445 *value = value.max(lower[i]).min(upper[i]);
446 }
447 }
448
449 fn environmental_selection(
451 &self,
452 mut population: Vec<MultiObjectiveSolution>,
453 ) -> Vec<MultiObjectiveSolution> {
454 let fronts = self.non_dominated_sorting(&mut population);
456
457 let mut new_population = Vec::new();
458 let mut front_idx = 0;
459
460 while front_idx < fronts.len()
462 && new_population.len() + fronts[front_idx].len() <= self.config.population_size
463 {
464 new_population.extend(fronts[front_idx].clone());
465 front_idx += 1;
466 }
467
468 if front_idx < fronts.len() && new_population.len() < self.config.population_size {
470 let mut last_front = fronts[front_idx].clone();
471 self.calculate_crowding_distance(&mut last_front);
472
473 last_front.sort_by(|a, b| {
475 b.crowding_distance
476 .partial_cmp(&a.crowding_distance)
477 .unwrap_or(Ordering::Equal)
478 });
479
480 let remaining = self.config.population_size - new_population.len();
481 new_population.extend(last_front.into_iter().take(remaining));
482 }
483
484 new_population
485 }
486
487 fn non_dominated_sorting(
489 &self,
490 population: &mut [MultiObjectiveSolution],
491 ) -> Vec<Vec<MultiObjectiveSolution>> {
492 let n = population.len();
493 let mut domination_count = vec![0; n];
494 let mut dominated_solutions = vec![Vec::new(); n];
495 let mut fronts = Vec::new();
496 let mut current_front = Vec::new();
497
498 for i in 0..n {
500 for j in 0..n {
501 if i != j {
502 match self.compare_dominance(&population[i], &population[j]) {
503 Ordering::Less => {
504 dominated_solutions[i].push(j);
505 }
506 Ordering::Greater => {
507 domination_count[i] += 1;
508 }
509 Ordering::Equal => {}
510 }
511 }
512 }
513
514 if domination_count[i] == 0 {
515 population[i].rank = 0;
516 current_front.push(population[i].clone());
517 }
518 }
519
520 let mut rank = 0;
521 while !current_front.is_empty() {
522 fronts.push(current_front.clone());
523 let mut next_front = Vec::new();
524
525 for i in 0..n {
526 if population[i].rank == rank {
527 for &j in &dominated_solutions[i] {
528 domination_count[j] -= 1;
529 if domination_count[j] == 0 {
530 population[j].rank = rank + 1;
531 next_front.push(population[j].clone());
532 }
533 }
534 }
535 }
536
537 current_front = next_front;
538 rank += 1;
539 }
540
541 fronts
542 }
543
544 fn compare_dominance(
546 &self,
547 a: &MultiObjectiveSolution,
548 b: &MultiObjectiveSolution,
549 ) -> Ordering {
550 if a.constraint_violation < b.constraint_violation {
552 return Ordering::Less;
553 } else if a.constraint_violation > b.constraint_violation {
554 return Ordering::Greater;
555 }
556
557 let mut a_better = false;
559 let mut b_better = false;
560
561 for i in 0..self.n_objectives {
562 if a.objectives[i] < b.objectives[i] {
563 a_better = true;
564 } else if a.objectives[i] > b.objectives[i] {
565 b_better = true;
566 }
567 }
568
569 if a_better && !b_better {
570 Ordering::Less
571 } else if b_better && !a_better {
572 Ordering::Greater
573 } else {
574 Ordering::Equal
575 }
576 }
577
578 fn calculate_crowding_distance(&self, front: &mut [MultiObjectiveSolution]) {
580 let n = front.len();
581
582 for solution in front.iter_mut() {
584 solution.crowding_distance = 0.0;
585 }
586
587 if n <= 2 {
588 for solution in front.iter_mut() {
589 solution.crowding_distance = f64::INFINITY;
590 }
591 return;
592 }
593
594 for obj in 0..self.n_objectives {
596 front.sort_by(|a, b| {
598 a.objectives[obj]
599 .partial_cmp(&b.objectives[obj])
600 .unwrap_or(Ordering::Equal)
601 });
602
603 front[0].crowding_distance = f64::INFINITY;
605 front[n - 1].crowding_distance = f64::INFINITY;
606
607 let obj_range = front[n - 1].objectives[obj] - front[0].objectives[obj];
608
609 if obj_range > 0.0 {
610 for i in 1..n - 1 {
611 front[i].crowding_distance +=
612 (front[i + 1].objectives[obj] - front[i - 1].objectives[obj]) / obj_range;
613 }
614 }
615 }
616 }
617
618 fn calculate_hypervolume(&self, reference_point: &Array1<f64>) -> Result<f64, OptimizeError> {
620 if reference_point.len() != self.n_objectives {
621 return Err(OptimizeError::ValueError(
622 "Reference point dimension must match number of objectives".to_string(),
623 ));
624 }
625
626 let pareto_front: Vec<&MultiObjectiveSolution> = self
627 .population
628 .iter()
629 .filter(|sol| sol.rank == 0 && sol.constraint_violation == 0.0)
630 .collect();
631
632 if pareto_front.is_empty() {
633 return Ok(0.0);
634 }
635
636 if self.n_objectives == 2 {
638 let mut points: Vec<_> = pareto_front
639 .iter()
640 .map(|sol| (sol.objectives[0], sol.objectives[1]))
641 .collect();
642
643 points.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
645
646 let mut volume = 0.0;
647 let mut prev_y = reference_point[1];
648
649 for &(x, y) in &points {
650 if x < reference_point[0] && y < reference_point[1] && y < prev_y {
651 volume += (reference_point[0] - x) * (prev_y - y);
652 prev_y = y;
653 }
654 }
655
656 Ok(volume)
657 } else {
658 self.hypervolume_monte_carlo(reference_point, &pareto_front)
660 }
661 }
662
663 fn hypervolume_monte_carlo(
665 &self,
666 reference_point: &Array1<f64>,
667 pareto_front: &[&MultiObjectiveSolution],
668 ) -> Result<f64, OptimizeError> {
669 let n_samples = 10000;
670 let mut rng = rng();
671 let mut dominated_count = 0;
672
673 let mut min_bounds = reference_point.clone();
675 for sol in pareto_front {
676 for i in 0..self.n_objectives {
677 min_bounds[i] = min_bounds[i].min(sol.objectives[i]);
678 }
679 }
680
681 for _ in 0..n_samples {
682 let mut point = Array1::zeros(self.n_objectives);
684 for i in 0..self.n_objectives {
685 point[i] =
686 min_bounds[i] + rng.random::<f64>() * (reference_point[i] - min_bounds[i]);
687 }
688
689 let mut is_dominated = false;
691 for sol in pareto_front {
692 let mut dominates = true;
693 for i in 0..self.n_objectives {
694 if sol.objectives[i] >= point[i] {
695 dominates = false;
696 break;
697 }
698 }
699 if dominates {
700 is_dominated = true;
701 break;
702 }
703 }
704
705 if is_dominated {
706 dominated_count += 1;
707 }
708 }
709
710 let mut total_volume = 1.0;
712 for i in 0..self.n_objectives {
713 total_volume *= reference_point[i] - min_bounds[i];
714 }
715
716 Ok(total_volume * dominated_count as f64 / n_samples as f64)
717 }
718}
719
720impl NSGAIII {
721 pub fn new(
723 n_variables: usize,
724 n_objectives: usize,
725 config: Option<MultiObjectiveConfig>,
726 ) -> Self {
727 let config = config.unwrap_or_default();
728 let reference_points = Self::generate_reference_points(n_objectives, 12); Self {
731 config,
732 n_objectives,
733 n_variables,
734 population: Vec::new(),
735 reference_points,
736 generation: 0,
737 n_evaluations: 0,
738 }
739 }
740
741 pub fn with_reference_points(
743 n_variables: usize,
744 n_objectives: usize,
745 reference_points: Array2<f64>,
746 config: Option<MultiObjectiveConfig>,
747 ) -> Result<Self, OptimizeError> {
748 if reference_points.ncols() != n_objectives {
749 return Err(OptimizeError::ValueError(
750 "Reference points must have same number of columns as objectives".to_string(),
751 ));
752 }
753
754 let config = config.unwrap_or_default();
755
756 Ok(Self {
757 config,
758 n_objectives,
759 n_variables,
760 population: Vec::new(),
761 reference_points,
762 generation: 0,
763 n_evaluations: 0,
764 })
765 }
766
767 pub fn with_bounds(
769 mut self,
770 lower: Array1<f64>,
771 upper: Array1<f64>,
772 ) -> Result<Self, OptimizeError> {
773 if lower.len() != self.n_variables || upper.len() != self.n_variables {
774 return Err(OptimizeError::ValueError(
775 "Bounds dimensions must match number of variables".to_string(),
776 ));
777 }
778
779 for (&l, &u) in lower.iter().zip(upper.iter()) {
780 if l >= u {
781 return Err(OptimizeError::ValueError(
782 "Lower bounds must be less than upper bounds".to_string(),
783 ));
784 }
785 }
786
787 self.config.bounds = Some((lower, upper));
788 Ok(self)
789 }
790
791 pub fn generate_reference_points(n_objectives: usize, n_divisions: usize) -> Array2<f64> {
793 let n_points = Self::binomial_coefficient(n_objectives + n_divisions - 1, n_divisions);
794 let mut points = Array2::zeros((n_points, n_objectives));
795 let mut point_idx = 0;
796
797 Self::generate_points_recursive(
798 &mut points,
799 &mut point_idx,
800 n_objectives,
801 n_divisions,
802 0,
803 Array1::zeros(n_objectives),
804 n_divisions,
805 );
806
807 for mut row in points.outer_iter_mut() {
809 let sum: f64 = row.sum();
810 if sum > 0.0 {
811 for val in row.iter_mut() {
812 *val /= sum;
813 }
814 }
815 }
816
817 points
818 }
819
820 fn generate_points_recursive(
822 points: &mut Array2<f64>,
823 point_idx: &mut usize,
824 n_objectives: usize,
825 _n_divisions: usize,
826 current_objective: usize,
827 mut current_point: Array1<f64>,
828 remaining_sum: usize,
829 ) {
830 if current_objective == n_objectives - 1 {
831 current_point[current_objective] = remaining_sum as f64;
832 if *point_idx < points.nrows() {
833 points.row_mut(*point_idx).assign(¤t_point);
834 *point_idx += 1;
835 }
836 return;
837 }
838
839 for i in 0..=remaining_sum {
840 current_point[current_objective] = i as f64;
841 Self::generate_points_recursive(
842 points,
843 point_idx,
844 n_objectives,
845 _n_divisions,
846 current_objective + 1,
847 current_point.clone(),
848 remaining_sum - i,
849 );
850 }
851 }
852
853 fn binomial_coefficient(n: usize, k: usize) -> usize {
855 if k > n {
856 return 0;
857 }
858 if k == 0 || k == n {
859 return 1;
860 }
861
862 let k = k.min(n - k); let mut result = 1;
864 for i in 0..k {
865 result = result * (n - i) / (i + 1);
866 }
867 result
868 }
869
870 pub fn optimize<F, C>(
872 &mut self,
873 mut objective_fn: F,
874 mut constraint_fn: Option<C>,
875 ) -> Result<MultiObjectiveResult, OptimizeError>
876 where
877 F: FnMut(&ArrayView1<f64>) -> Array1<f64>,
878 C: FnMut(&ArrayView1<f64>) -> f64,
879 {
880 self.initialize_population(&mut objective_fn, constraint_fn.as_mut())?;
882
883 for generation in 0..self.config.max_generations {
884 self.generation = generation;
885
886 if let Some(max_evals) = self.config.max_evaluations {
888 if self.n_evaluations >= max_evals {
889 break;
890 }
891 }
892
893 let offspring = self.create_offspring(&mut objective_fn, constraint_fn.as_mut())?;
895
896 let mut combined_population = self.population.clone();
898 combined_population.extend(offspring);
899
900 self.population = self.environmental_selection_nsga3(combined_population);
902 }
903
904 let pareto_front: Vec<MultiObjectiveSolution> = self
906 .population
907 .iter()
908 .filter(|sol| sol.rank == 0)
909 .cloned()
910 .collect();
911
912 let hypervolume = if let Some(ref reference_point) = self.config.reference_point {
913 Some(self.calculate_hypervolume(reference_point)?)
914 } else {
915 None
916 };
917
918 Ok(MultiObjectiveResult {
919 pareto_front,
920 population: self.population.clone(),
921 n_evaluations: self.n_evaluations,
922 n_generations: self.generation + 1,
923 success: true,
924 message: "NSGA-III optimization completed successfully".to_string(),
925 hypervolume,
926 })
927 }
928
929 fn initialize_population<F, C>(
931 &mut self,
932 objective_fn: &mut F,
933 mut constraint_fn: Option<&mut C>,
934 ) -> Result<(), OptimizeError>
935 where
936 F: FnMut(&ArrayView1<f64>) -> Array1<f64>,
937 C: FnMut(&ArrayView1<f64>) -> f64,
938 {
939 self.population.clear();
940 let mut rng = rng();
941
942 for _ in 0..self.config.population_size {
943 let variables = if let Some((ref lower, ref upper)) = self.config.bounds {
944 Array1::from_shape_fn(self.n_variables, |i| {
945 lower[i] + rng.random::<f64>() * (upper[i] - lower[i])
946 })
947 } else {
948 Array1::from_shape_fn(self.n_variables, |_| rng.random::<f64>() * 2.0 - 1.0)
949 };
950
951 let objectives = objective_fn(&variables.view());
952 self.n_evaluations += 1;
953
954 let constraint_violation = if let Some(ref mut constraint_fn) = constraint_fn {
955 constraint_fn(&variables.view()).max(0.0)
956 } else {
957 0.0
958 };
959
960 let solution = MultiObjectiveSolution {
961 variables,
962 objectives,
963 constraint_violation,
964 rank: 0,
965 crowding_distance: 0.0,
966 metadata: HashMap::new(),
967 };
968
969 self.population.push(solution);
970 }
971
972 Ok(())
973 }
974
975 fn create_offspring<F, C>(
977 &mut self,
978 objective_fn: &mut F,
979 mut constraint_fn: Option<&mut C>,
980 ) -> Result<Vec<MultiObjectiveSolution>, OptimizeError>
981 where
982 F: FnMut(&ArrayView1<f64>) -> Array1<f64>,
983 C: FnMut(&ArrayView1<f64>) -> f64,
984 {
985 let mut offspring = Vec::new();
986 let mut rng = rng();
987
988 while offspring.len() < self.config.population_size {
989 let parent1 = self.tournament_selection(&mut rng);
991 let parent2 = self.tournament_selection(&mut rng);
992
993 let (mut child1_vars, mut child2_vars) = if rng.random::<f64>()
995 < self.config.crossover_probability
996 {
997 self.simulated_binary_crossover(&parent1.variables, &parent2.variables, &mut rng)
998 } else {
999 (parent1.variables.clone(), parent2.variables.clone())
1000 };
1001
1002 if rng.random::<f64>() < self.config.mutation_probability {
1004 self.polynomial_mutation(&mut child1_vars, &mut rng);
1005 }
1006 if rng.random::<f64>() < self.config.mutation_probability {
1007 self.polynomial_mutation(&mut child2_vars, &mut rng);
1008 }
1009
1010 if let Some((ref lower, ref upper)) = self.config.bounds {
1012 self.apply_bounds(&mut child1_vars, lower, upper);
1013 self.apply_bounds(&mut child2_vars, lower, upper);
1014 }
1015
1016 for child_vars in [child1_vars, child2_vars] {
1018 if offspring.len() >= self.config.population_size {
1019 break;
1020 }
1021
1022 let objectives = objective_fn(&child_vars.view());
1023 self.n_evaluations += 1;
1024
1025 let constraint_violation = if let Some(ref mut constraint_fn) = constraint_fn {
1026 constraint_fn(&child_vars.view()).max(0.0)
1027 } else {
1028 0.0
1029 };
1030
1031 let solution = MultiObjectiveSolution {
1032 variables: child_vars,
1033 objectives,
1034 constraint_violation,
1035 rank: 0,
1036 crowding_distance: 0.0,
1037 metadata: HashMap::new(),
1038 };
1039
1040 offspring.push(solution);
1041 }
1042 }
1043
1044 Ok(offspring)
1045 }
1046
1047 fn environmental_selection_nsga3(
1049 &self,
1050 mut population: Vec<MultiObjectiveSolution>,
1051 ) -> Vec<MultiObjectiveSolution> {
1052 let fronts = self.non_dominated_sorting(&mut population);
1054
1055 let mut new_population = Vec::new();
1056 let mut front_idx = 0;
1057
1058 while front_idx < fronts.len()
1060 && new_population.len() + fronts[front_idx].len() <= self.config.population_size
1061 {
1062 new_population.extend(fronts[front_idx].clone());
1063 front_idx += 1;
1064 }
1065
1066 if front_idx < fronts.len() && new_population.len() < self.config.population_size {
1068 let remaining = self.config.population_size - new_population.len();
1069 let partial_front = &fronts[front_idx];
1070
1071 let all_solutions: Vec<_> = new_population.iter().chain(partial_front.iter()).collect();
1073 let normalized_objectives = self.normalize_objectives(&all_solutions);
1074
1075 let associations = self.associate_with_reference_points(
1077 &normalized_objectives,
1078 &new_population,
1079 partial_front,
1080 );
1081
1082 let selected = self.reference_point_selection(partial_front, associations, remaining);
1084 new_population.extend(selected);
1085 }
1086
1087 new_population
1088 }
1089
1090 fn normalize_objectives(&self, solutions: &[&MultiObjectiveSolution]) -> Array2<f64> {
1092 let n_solutions = solutions.len();
1093 let mut normalized = Array2::zeros((n_solutions, self.n_objectives));
1094
1095 let mut ideal_point = Array1::from_elem(self.n_objectives, f64::INFINITY);
1097 let mut nadir_point = Array1::from_elem(self.n_objectives, f64::NEG_INFINITY);
1098
1099 for solution in solutions {
1100 for (i, &obj_val) in solution.objectives.iter().enumerate() {
1101 ideal_point[i] = ideal_point[i].min(obj_val);
1102 nadir_point[i] = nadir_point[i].max(obj_val);
1103 }
1104 }
1105
1106 for (sol_idx, solution) in solutions.iter().enumerate() {
1108 for (obj_idx, &obj_val) in solution.objectives.iter().enumerate() {
1109 let range = nadir_point[obj_idx] - ideal_point[obj_idx];
1110 normalized[[sol_idx, obj_idx]] = if range > 1e-12 {
1111 (obj_val - ideal_point[obj_idx]) / range
1112 } else {
1113 0.0
1114 };
1115 }
1116 }
1117
1118 normalized
1119 }
1120
1121 fn associate_with_reference_points(
1123 &self,
1124 normalized_objectives: &Array2<f64>,
1125 new_population: &[MultiObjectiveSolution],
1126 partial_front: &[MultiObjectiveSolution],
1127 ) -> Vec<Vec<usize>> {
1128 let n_ref_points = self.reference_points.nrows();
1129 let mut associations = vec![Vec::new(); n_ref_points];
1130
1131 let mut niche_count = vec![0; n_ref_points];
1133 for (sol_idx, _) in new_population.iter().enumerate() {
1134 let ref_point_idx = self.find_closest_reference_point(sol_idx, normalized_objectives);
1135 niche_count[ref_point_idx] += 1;
1136 }
1137
1138 let start_idx = new_population.len();
1140 for (i, _) in partial_front.iter().enumerate() {
1141 let sol_idx = start_idx + i;
1142 let ref_point_idx = self.find_closest_reference_point(sol_idx, normalized_objectives);
1143 associations[ref_point_idx].push(i);
1144 }
1145
1146 associations
1147 }
1148
1149 fn find_closest_reference_point(
1151 &self,
1152 solution_idx: usize,
1153 normalized_objectives: &Array2<f64>,
1154 ) -> usize {
1155 let mut min_distance = f64::INFINITY;
1156 let mut closest_ref_point = 0;
1157
1158 for (ref_idx, ref_point) in self.reference_points.outer_iter().enumerate() {
1159 let distance =
1160 self.perpendicular_distance(&normalized_objectives.row(solution_idx), &ref_point);
1161
1162 if distance < min_distance {
1163 min_distance = distance;
1164 closest_ref_point = ref_idx;
1165 }
1166 }
1167
1168 closest_ref_point
1169 }
1170
1171 fn perpendicular_distance(
1173 &self,
1174 point: &ArrayView1<f64>,
1175 reference_point: &ArrayView1<f64>,
1176 ) -> f64 {
1177 let dot_product: f64 = point
1179 .iter()
1180 .zip(reference_point.iter())
1181 .map(|(&p, &r)| p * r)
1182 .sum();
1183 let ref_norm_sq: f64 = reference_point.iter().map(|&r| r * r).sum();
1184
1185 if ref_norm_sq < 1e-12 {
1186 return point.iter().map(|&p| p * p).sum::<f64>().sqrt();
1187 }
1188
1189 let projection_length = dot_product / ref_norm_sq.sqrt();
1190
1191 let point_norm: f64 = point.iter().map(|&p| p * p).sum::<f64>().sqrt();
1193
1194 if projection_length >= point_norm {
1195 0.0
1196 } else {
1197 (point_norm * point_norm - projection_length * projection_length).sqrt()
1198 }
1199 }
1200
1201 fn reference_point_selection(
1203 &self,
1204 partial_front: &[MultiObjectiveSolution],
1205 associations: Vec<Vec<usize>>,
1206 k: usize,
1207 ) -> Vec<MultiObjectiveSolution> {
1208 let mut selected = Vec::new();
1209 let mut niche_count = vec![0; self.reference_points.nrows()];
1210
1211 for _ in 0..k {
1214 let mut min_niche_count = usize::MAX;
1216 let mut selected_ref_points = Vec::new();
1217
1218 for (ref_idx, associated_solutions) in associations.iter().enumerate() {
1219 if !associated_solutions.is_empty() && niche_count[ref_idx] < min_niche_count {
1220 min_niche_count = niche_count[ref_idx];
1221 selected_ref_points.clear();
1222 selected_ref_points.push(ref_idx);
1223 } else if !associated_solutions.is_empty()
1224 && niche_count[ref_idx] == min_niche_count
1225 {
1226 selected_ref_points.push(ref_idx);
1227 }
1228 }
1229
1230 if selected_ref_points.is_empty() {
1231 break;
1232 }
1233
1234 let mut rng = rng();
1236 let ref_point_idx = selected_ref_points[rng.random_range(0..selected_ref_points.len())];
1237
1238 let associated_indices = &associations[ref_point_idx];
1240 if !associated_indices.is_empty() {
1241 let sol_idx = if niche_count[ref_point_idx] == 0 {
1242 associated_indices[0] } else {
1245 associated_indices[rng.random_range(0..associated_indices.len())]
1247 };
1248
1249 selected.push(partial_front[sol_idx].clone());
1250 niche_count[ref_point_idx] += 1;
1251
1252 }
1255 }
1256
1257 selected
1258 }
1259
1260 fn non_dominated_sorting(
1262 &self,
1263 population: &mut [MultiObjectiveSolution],
1264 ) -> Vec<Vec<MultiObjectiveSolution>> {
1265 let n = population.len();
1266 let mut domination_count = vec![0; n];
1267 let mut dominated_solutions = vec![Vec::new(); n];
1268 let mut fronts = Vec::new();
1269 let mut current_front = Vec::new();
1270
1271 for i in 0..n {
1273 for j in 0..n {
1274 if i != j {
1275 match self.compare_dominance(&population[i], &population[j]) {
1276 Ordering::Less => {
1277 dominated_solutions[i].push(j);
1278 }
1279 Ordering::Greater => {
1280 domination_count[i] += 1;
1281 }
1282 Ordering::Equal => {}
1283 }
1284 }
1285 }
1286
1287 if domination_count[i] == 0 {
1288 population[i].rank = 0;
1289 current_front.push(population[i].clone());
1290 }
1291 }
1292
1293 let mut rank = 0;
1294 while !current_front.is_empty() {
1295 fronts.push(current_front.clone());
1296 let mut next_front = Vec::new();
1297
1298 for i in 0..n {
1299 if population[i].rank == rank {
1300 for &j in &dominated_solutions[i] {
1301 domination_count[j] -= 1;
1302 if domination_count[j] == 0 {
1303 population[j].rank = rank + 1;
1304 next_front.push(population[j].clone());
1305 }
1306 }
1307 }
1308 }
1309
1310 current_front = next_front;
1311 rank += 1;
1312 }
1313
1314 fronts
1315 }
1316
1317 fn compare_dominance(
1319 &self,
1320 a: &MultiObjectiveSolution,
1321 b: &MultiObjectiveSolution,
1322 ) -> Ordering {
1323 if a.constraint_violation < b.constraint_violation {
1325 return Ordering::Less;
1326 } else if a.constraint_violation > b.constraint_violation {
1327 return Ordering::Greater;
1328 }
1329
1330 let mut a_better = false;
1332 let mut b_better = false;
1333
1334 for i in 0..self.n_objectives {
1335 if a.objectives[i] < b.objectives[i] {
1336 a_better = true;
1337 } else if a.objectives[i] > b.objectives[i] {
1338 b_better = true;
1339 }
1340 }
1341
1342 if a_better && !b_better {
1343 Ordering::Less
1344 } else if b_better && !a_better {
1345 Ordering::Greater
1346 } else {
1347 Ordering::Equal
1348 }
1349 }
1350
1351 fn tournament_selection(&self, rng: &mut impl Rng) -> &MultiObjectiveSolution {
1353 let tournament_size = 2;
1354 let mut best_idx = rng.random_range(0..self.population.len());
1355
1356 for _ in 1..tournament_size {
1357 let idx = rng.random_range(0..self.population.len());
1358 if self.dominates_or_better(&self.population[idx], &self.population[best_idx]) {
1359 best_idx = idx;
1360 }
1361 }
1362
1363 &self.population[best_idx]
1364 }
1365
1366 fn dominates_or_better(&self, a: &MultiObjectiveSolution, b: &MultiObjectiveSolution) -> bool {
1368 if a.constraint_violation < b.constraint_violation {
1370 return true;
1371 } else if a.constraint_violation > b.constraint_violation {
1372 return false;
1373 }
1374
1375 match a.rank.cmp(&b.rank) {
1377 std::cmp::Ordering::Less => true,
1378 std::cmp::Ordering::Equal => a.crowding_distance > b.crowding_distance,
1379 std::cmp::Ordering::Greater => false,
1380 }
1381 }
1382
1383 fn simulated_binary_crossover(
1385 &self,
1386 parent1: &Array1<f64>,
1387 parent2: &Array1<f64>,
1388 rng: &mut impl Rng,
1389 ) -> (Array1<f64>, Array1<f64>) {
1390 let mut child1 = parent1.clone();
1391 let mut child2 = parent2.clone();
1392 let eta = self.config.crossover_eta;
1393
1394 for i in 0..self.n_variables {
1395 if rng.random::<f64>() <= 0.5 {
1396 let y1 = parent1[i].min(parent2[i]);
1397 let y2 = parent1[i].max(parent2[i]);
1398
1399 if (y2 - y1).abs() > 1e-14 {
1400 let u = rng.random::<f64>();
1401 let beta = if u <= 0.5 {
1402 (2.0 * u).powf(1.0 / (eta + 1.0))
1403 } else {
1404 (1.0 / (2.0 * (1.0 - u))).powf(1.0 / (eta + 1.0))
1405 };
1406
1407 child1[i] = 0.5 * ((y1 + y2) - beta * (y2 - y1));
1408 child2[i] = 0.5 * ((y1 + y2) + beta * (y2 - y1));
1409 }
1410 }
1411 }
1412
1413 (child1, child2)
1414 }
1415
1416 fn polynomial_mutation(&self, individual: &mut Array1<f64>, rng: &mut impl Rng) {
1418 let eta = self.config.mutation_eta;
1419
1420 for i in 0..self.n_variables {
1421 if rng.random::<f64>() <= 1.0 / self.n_variables as f64 {
1422 let u = rng.random::<f64>();
1423 let delta = if u < 0.5 {
1424 (2.0 * u).powf(1.0 / (eta + 1.0)) - 1.0
1425 } else {
1426 1.0 - (2.0 * (1.0 - u)).powf(1.0 / (eta + 1.0))
1427 };
1428
1429 if let Some((ref lower, ref upper)) = self.config.bounds {
1430 let range = upper[i] - lower[i];
1431 individual[i] += delta * range * 0.1;
1432 } else {
1433 individual[i] += delta * 0.1;
1434 }
1435 }
1436 }
1437 }
1438
1439 fn apply_bounds(&self, individual: &mut Array1<f64>, lower: &Array1<f64>, upper: &Array1<f64>) {
1441 for (i, value) in individual.iter_mut().enumerate() {
1442 *value = value.max(lower[i]).min(upper[i]);
1443 }
1444 }
1445
1446 fn calculate_hypervolume(&self, reference_point: &Array1<f64>) -> Result<f64, OptimizeError> {
1448 if reference_point.len() != self.n_objectives {
1449 return Err(OptimizeError::ValueError(
1450 "Reference point dimension must match number of objectives".to_string(),
1451 ));
1452 }
1453
1454 let pareto_front: Vec<&MultiObjectiveSolution> = self
1455 .population
1456 .iter()
1457 .filter(|sol| sol.rank == 0 && sol.constraint_violation == 0.0)
1458 .collect();
1459
1460 if pareto_front.is_empty() {
1461 return Ok(0.0);
1462 }
1463
1464 self.hypervolume_monte_carlo(reference_point, &pareto_front)
1466 }
1467
1468 fn hypervolume_monte_carlo(
1470 &self,
1471 reference_point: &Array1<f64>,
1472 pareto_front: &[&MultiObjectiveSolution],
1473 ) -> Result<f64, OptimizeError> {
1474 let n_samples = 100000; let mut rng = rng();
1476 let mut dominated_count = 0;
1477
1478 let mut min_bounds = reference_point.clone();
1480 for sol in pareto_front {
1481 for i in 0..self.n_objectives {
1482 min_bounds[i] = min_bounds[i].min(sol.objectives[i]);
1483 }
1484 }
1485
1486 for _ in 0..n_samples {
1487 let mut point = Array1::zeros(self.n_objectives);
1489 for i in 0..self.n_objectives {
1490 point[i] =
1491 min_bounds[i] + rng.random::<f64>() * (reference_point[i] - min_bounds[i]);
1492 }
1493
1494 let mut is_dominated = false;
1496 for sol in pareto_front {
1497 let mut dominates = true;
1498 for i in 0..self.n_objectives {
1499 if sol.objectives[i] >= point[i] {
1500 dominates = false;
1501 break;
1502 }
1503 }
1504 if dominates {
1505 is_dominated = true;
1506 break;
1507 }
1508 }
1509
1510 if is_dominated {
1511 dominated_count += 1;
1512 }
1513 }
1514
1515 let mut total_volume = 1.0;
1517 for i in 0..self.n_objectives {
1518 total_volume *= reference_point[i] - min_bounds[i];
1519 }
1520
1521 Ok(total_volume * dominated_count as f64 / n_samples as f64)
1522 }
1523}
1524
1525pub mod scalarization {
1527 use super::*;
1528
1529 pub fn weighted_sum<F>(objectives_fn: F, weights: &Array1<f64>, x: &ArrayView1<f64>) -> f64
1531 where
1532 F: Fn(&ArrayView1<f64>) -> Array1<f64>,
1533 {
1534 let objectives = objectives_fn(x);
1535 objectives
1536 .iter()
1537 .zip(weights.iter())
1538 .map(|(&obj, &w)| w * obj)
1539 .sum()
1540 }
1541
1542 pub fn weighted_tchebycheff<F>(
1544 objectives_fn: F,
1545 weights: &Array1<f64>,
1546 ideal_point: &Array1<f64>,
1547 x: &ArrayView1<f64>,
1548 ) -> f64
1549 where
1550 F: Fn(&ArrayView1<f64>) -> Array1<f64>,
1551 {
1552 let objectives = objectives_fn(x);
1553 objectives
1554 .iter()
1555 .zip(weights.iter())
1556 .zip(ideal_point.iter())
1557 .map(|((&obj, &w), &ideal)| w * (obj - ideal).abs())
1558 .fold(f64::NEG_INFINITY, f64::max)
1559 }
1560
1561 pub fn achievement_scalarizing<F>(
1563 objectives_fn: F,
1564 weights: &Array1<f64>,
1565 reference_point: &Array1<f64>,
1566 x: &ArrayView1<f64>,
1567 ) -> f64
1568 where
1569 F: Fn(&ArrayView1<f64>) -> Array1<f64>,
1570 {
1571 let objectives = objectives_fn(x);
1572 let rho = 1e-6; let max_term = objectives
1575 .iter()
1576 .zip(weights.iter())
1577 .zip(reference_point.iter())
1578 .map(|((&obj, &w), &ref_val)| (obj - ref_val) / w)
1579 .fold(f64::NEG_INFINITY, f64::max);
1580
1581 let sum_term: f64 = objectives
1582 .iter()
1583 .zip(reference_point.iter())
1584 .map(|(&obj, &ref_val)| obj - ref_val)
1585 .sum();
1586
1587 max_term + rho * sum_term
1588 }
1589
1590 pub struct EpsilonConstraint {
1592 pub primary_objective: usize,
1594 pub epsilon_constraints: Array1<f64>,
1596 }
1597
1598 impl EpsilonConstraint {
1599 pub fn new(primary_objective: usize, epsilon_constraints: Array1<f64>) -> Self {
1600 Self {
1601 primary_objective,
1602 epsilon_constraints,
1603 }
1604 }
1605
1606 pub fn scalarize<F>(
1608 &self,
1609 objectives_fn: F,
1610 penalty_weight: f64,
1611 ) -> impl Fn(&ArrayView1<f64>) -> f64
1612 where
1613 F: Fn(&ArrayView1<f64>) -> Array1<f64> + Clone,
1614 {
1615 let primary_obj = self.primary_objective;
1616 let constraints = self.epsilon_constraints.clone();
1617
1618 move |x: &ArrayView1<f64>| -> f64 {
1619 let objectives = objectives_fn(x);
1620 let mut result = objectives[primary_obj];
1621
1622 for (i, &eps) in constraints.iter().enumerate() {
1624 let obj_idx = if i >= primary_obj { i + 1 } else { i };
1625 if obj_idx < objectives.len() && objectives[obj_idx] > eps {
1626 result += penalty_weight * (objectives[obj_idx] - eps).powi(2);
1627 }
1628 }
1629
1630 result
1631 }
1632 }
1633 }
1634}
1635
1636#[cfg(test)]
1637mod tests {
1638 use super::*;
1639 use approx::assert_abs_diff_eq;
1640 use ndarray::array;
1641
1642 #[test]
1643 fn test_nsga2_basic() {
1644 let objective_fn = |x: &ArrayView1<f64>| array![x[0].powi(2), (x[0] - 1.0).powi(2)];
1646
1647 let config = MultiObjectiveConfig {
1648 population_size: 50,
1649 max_generations: 50,
1650 ..Default::default()
1651 };
1652
1653 let mut optimizer = NSGAII::new(1, 2, Some(config))
1654 .with_bounds(array![-2.0], array![2.0])
1655 .unwrap();
1656
1657 let result = optimizer
1658 .optimize(objective_fn, None::<fn(&ArrayView1<f64>) -> f64>)
1659 .unwrap();
1660
1661 assert!(result.success);
1662 assert!(!result.pareto_front.is_empty());
1663 assert!(result.n_evaluations > 0);
1664
1665 for solution in &result.pareto_front {
1667 assert!(solution.variables[0] >= -2.0 && solution.variables[0] <= 2.0);
1668 assert!(solution.constraint_violation == 0.0);
1669 assert!(solution.rank == 0);
1670 }
1671 }
1672
1673 #[test]
1674 fn test_dominance_comparison() {
1675 let optimizer = NSGAII::new(2, 2, None);
1676
1677 let sol1 = MultiObjectiveSolution {
1678 variables: array![0.0, 0.0],
1679 objectives: array![1.0, 2.0],
1680 constraint_violation: 0.0,
1681 rank: 0,
1682 crowding_distance: 0.0,
1683 metadata: HashMap::new(),
1684 };
1685
1686 let sol2 = MultiObjectiveSolution {
1687 variables: array![1.0, 1.0],
1688 objectives: array![2.0, 1.0],
1689 constraint_violation: 0.0,
1690 rank: 0,
1691 crowding_distance: 0.0,
1692 metadata: HashMap::new(),
1693 };
1694
1695 let sol3 = MultiObjectiveSolution {
1696 variables: array![0.5, 0.5],
1697 objectives: array![0.5, 1.5],
1698 constraint_violation: 0.0,
1699 rank: 0,
1700 crowding_distance: 0.0,
1701 metadata: HashMap::new(),
1702 };
1703
1704 assert_eq!(optimizer.compare_dominance(&sol3, &sol1), Ordering::Less);
1706 assert_eq!(optimizer.compare_dominance(&sol3, &sol2), Ordering::Equal);
1707
1708 assert_eq!(optimizer.compare_dominance(&sol1, &sol2), Ordering::Equal);
1710 }
1711
1712 #[test]
1713 fn test_scalarization_methods() {
1714 let objectives_fn = |x: &ArrayView1<f64>| array![x[0].powi(2), x[1].powi(2)];
1715 let x = array![1.0, 2.0];
1716 let weights = array![0.5, 0.5];
1717 let ideal_point = array![0.0, 0.0];
1718
1719 let weighted_result = scalarization::weighted_sum(objectives_fn, &weights, &x.view());
1721 assert_abs_diff_eq!(weighted_result, 2.5, epsilon = 1e-10); let tcheby_result =
1725 scalarization::weighted_tchebycheff(objectives_fn, &weights, &ideal_point, &x.view());
1726 assert_abs_diff_eq!(tcheby_result, 2.0, epsilon = 1e-10); }
1728
1729 #[test]
1730 fn test_hypervolume_2d() {
1731 let config = MultiObjectiveConfig::default();
1732 let optimizer = NSGAII::new(1, 2, Some(config));
1733
1734 let population = vec![
1736 MultiObjectiveSolution {
1737 variables: array![0.0],
1738 objectives: array![1.0, 3.0],
1739 constraint_violation: 0.0,
1740 rank: 0,
1741 crowding_distance: 0.0,
1742 metadata: HashMap::new(),
1743 },
1744 MultiObjectiveSolution {
1745 variables: array![1.0],
1746 objectives: array![2.0, 2.0],
1747 constraint_violation: 0.0,
1748 rank: 0,
1749 crowding_distance: 0.0,
1750 metadata: HashMap::new(),
1751 },
1752 MultiObjectiveSolution {
1753 variables: array![2.0],
1754 objectives: array![3.0, 1.0],
1755 constraint_violation: 0.0,
1756 rank: 0,
1757 crowding_distance: 0.0,
1758 metadata: HashMap::new(),
1759 },
1760 ];
1761
1762 let optimizer_with_pop = NSGAII {
1763 population,
1764 ..optimizer
1765 };
1766
1767 let reference_point = array![4.0, 4.0];
1768 let hypervolume = optimizer_with_pop
1769 .calculate_hypervolume(&reference_point)
1770 .unwrap();
1771
1772 assert!(hypervolume > 0.0);
1773 assert!(hypervolume < 16.0); }
1775}