amari_optimization/
multiobjective.rs

1//! # Multi-objective Pareto Optimization
2//!
3//! This module implements advanced multi-objective optimization algorithms, focusing on
4//! Pareto-optimal solutions and evolutionary approaches for handling conflicting objectives.
5//!
6//! ## Mathematical Background
7//!
8//! Multi-objective optimization deals with problems of the form:
9//!
10//! ```text
11//! minimize f(x) = [f₁(x), f₂(x), ..., fₘ(x)]ᵀ
12//! subject to g(x) ≤ 0, h(x) = 0
13//! ```
14//!
15//! where multiple objectives f₁, f₂, ..., fₘ are optimized simultaneously.
16//!
17//! ## Key Concepts
18//!
19//! - **Pareto Dominance**: Solution x dominates y if x is no worse in all objectives
20//!   and strictly better in at least one objective
21//! - **Pareto Front**: Set of all non-dominated solutions
22//! - **Hypervolume**: Volume of objective space dominated by a solution set
23//! - **Crowding Distance**: Measure of solution density for diversity preservation
24//!
25//! ## Algorithms
26//!
27//! - **NSGA-II**: Non-dominated Sorting Genetic Algorithm II
28//! - **NSGA-III**: Many-objective optimization with reference points
29//! - **MOEA/D**: Multi-objective Evolutionary Algorithm based on Decomposition
30//! - **Hypervolume-based Selection**: HypE, SMS-EMOA variants
31
32use crate::phantom::{MultiObjective, OptimizationProblem};
33use crate::OptimizationResult;
34
35use num_traits::Float;
36use rand::prelude::*;
37use std::cmp::Ordering;
38use std::marker::PhantomData;
39
40/// Configuration for multi-objective optimization algorithms
41#[derive(Clone, Debug)]
42pub struct MultiObjectiveConfig<T: Float> {
43    /// Population size for evolutionary algorithms
44    pub population_size: usize,
45    /// Maximum number of generations
46    pub max_generations: usize,
47    /// Crossover probability
48    pub crossover_probability: T,
49    /// Mutation probability
50    pub mutation_probability: T,
51    /// Mutation strength (standard deviation)
52    pub mutation_strength: T,
53    /// Elite preservation ratio
54    pub elite_ratio: T,
55    /// Convergence tolerance for hypervolume
56    pub convergence_tolerance: T,
57    /// Reference point for hypervolume calculation
58    pub reference_point: Option<Vec<T>>,
59    /// Enable diversity preservation mechanisms
60    pub preserve_diversity: bool,
61}
62
63impl<T: Float> Default for MultiObjectiveConfig<T> {
64    fn default() -> Self {
65        Self {
66            population_size: 100,
67            max_generations: 200, // Reduced but still sufficient
68            crossover_probability: T::from(0.9).unwrap(),
69            mutation_probability: T::from(0.2).unwrap(), // Increased for more diversity
70            mutation_strength: T::from(0.1).unwrap(),
71            elite_ratio: T::from(0.1).unwrap(),
72            convergence_tolerance: T::from(1e-4).unwrap(), // Relaxed for better diversity
73            reference_point: Some(vec![T::from(10.0).unwrap(), T::from(10.0).unwrap()]), // Default reference
74            preserve_diversity: true,
75        }
76    }
77}
78
79/// Individual solution in multi-objective optimization
80#[derive(Clone, Debug)]
81pub struct Individual<T: Float> {
82    /// Decision variables
83    pub variables: Vec<T>,
84    /// Objective function values
85    pub objectives: Vec<T>,
86    /// Domination rank (0 = non-dominated)
87    pub rank: usize,
88    /// Crowding distance for diversity
89    pub crowding_distance: T,
90    /// Constraint violations (if any)
91    pub constraint_violations: Vec<T>,
92}
93
94impl<T: Float> Individual<T> {
95    /// Create a new individual with given variables
96    pub fn new(variables: Vec<T>) -> Self {
97        Self {
98            variables,
99            objectives: Vec::new(),
100            rank: 0,
101            crowding_distance: T::zero(),
102            constraint_violations: Vec::new(),
103        }
104    }
105
106    /// Check if this individual dominates another
107    pub fn dominates(&self, other: &Individual<T>) -> bool {
108        if self.objectives.len() != other.objectives.len() {
109            return false;
110        }
111
112        let mut at_least_one_better = false;
113        for (my_obj, other_obj) in self.objectives.iter().zip(other.objectives.iter()) {
114            if *my_obj > *other_obj {
115                return false; // Assuming minimization
116            }
117            if *my_obj < *other_obj {
118                at_least_one_better = true;
119            }
120        }
121
122        at_least_one_better
123    }
124
125    /// Check if solutions are feasible (no constraint violations)
126    pub fn is_feasible(&self) -> bool {
127        self.constraint_violations.iter().all(|&v| v <= T::zero())
128    }
129}
130
131/// Pareto front representation
132#[derive(Clone, Debug)]
133pub struct ParetoFront<T: Float> {
134    /// Non-dominated solutions
135    pub solutions: Vec<Individual<T>>,
136    /// Hypervolume indicator
137    pub hypervolume: Option<T>,
138    /// Reference point used for hypervolume
139    pub reference_point: Option<Vec<T>>,
140}
141
142impl<T: Float> ParetoFront<T> {
143    /// Create new empty Pareto front
144    pub fn new() -> Self {
145        Self {
146            solutions: Vec::new(),
147            hypervolume: None,
148            reference_point: None,
149        }
150    }
151
152    /// Add a solution to the front if it's non-dominated
153    pub fn add_if_non_dominated(&mut self, candidate: Individual<T>) -> bool {
154        // Check if candidate is dominated by any existing solution
155        for existing in &self.solutions {
156            if existing.dominates(&candidate) {
157                return false; // Dominated, don't add
158            }
159        }
160
161        // Remove any existing solutions dominated by candidate
162        self.solutions
163            .retain(|existing| !candidate.dominates(existing));
164
165        // Add the candidate
166        self.solutions.push(candidate);
167        true
168    }
169
170    /// Calculate hypervolume indicator
171    pub fn calculate_hypervolume(&mut self, reference_point: &[T]) -> T {
172        if self.solutions.is_empty() {
173            return T::zero();
174        }
175
176        // Simplified hypervolume calculation for 2D case
177        if self.solutions[0].objectives.len() == 2 {
178            self.hypervolume_2d(reference_point)
179        } else {
180            // For higher dimensions, use approximation
181            self.hypervolume_approximation(reference_point)
182        }
183    }
184
185    /// 2D hypervolume calculation
186    fn hypervolume_2d(&self, reference_point: &[T]) -> T {
187        if self.solutions.len() == 1 {
188            let sol = &self.solutions[0];
189            return (reference_point[0] - sol.objectives[0])
190                * (reference_point[1] - sol.objectives[1]);
191        }
192
193        // Sort by first objective
194        let mut sorted_solutions = self.solutions.clone();
195        sorted_solutions.sort_by(|a, b| {
196            a.objectives[0]
197                .partial_cmp(&b.objectives[0])
198                .unwrap_or(Ordering::Equal)
199        });
200
201        let mut volume = T::zero();
202        let mut prev_y = reference_point[1];
203
204        for solution in &sorted_solutions {
205            let width = reference_point[0] - solution.objectives[0];
206            let height = prev_y - solution.objectives[1];
207            volume = volume + width * height;
208            prev_y = solution.objectives[1];
209        }
210
211        volume
212    }
213
214    /// Approximate hypervolume for higher dimensions
215    fn hypervolume_approximation(&self, reference_point: &[T]) -> T {
216        // Monte Carlo approximation
217        let samples = 10000;
218        let mut dominated_count = 0;
219        let mut rng = thread_rng();
220
221        let obj_count = self.solutions[0].objectives.len();
222
223        for _ in 0..samples {
224            // Generate random point in objective space
225            let random_point: Vec<T> = (0..obj_count)
226                .map(|i| {
227                    let min_obj = self
228                        .solutions
229                        .iter()
230                        .map(|s| s.objectives[i])
231                        .fold(T::infinity(), |a, b| if a < b { a } else { b });
232                    let range = reference_point[i] - min_obj;
233                    min_obj + T::from(rng.gen::<f64>()).unwrap() * range
234                })
235                .collect();
236
237            // Check if any solution dominates this random point
238            for solution in &self.solutions {
239                let dominates = solution
240                    .objectives
241                    .iter()
242                    .zip(random_point.iter())
243                    .all(|(&obj, &rand)| obj <= rand);
244
245                if dominates {
246                    dominated_count += 1;
247                    break;
248                }
249            }
250        }
251
252        // Calculate volume
253        let total_volume = reference_point
254            .iter()
255            .zip(self.solutions.iter().flat_map(|s| s.objectives.iter()))
256            .fold(T::one(), |acc, (&ref_val, &_obj_val)| acc * ref_val);
257
258        total_volume * T::from(dominated_count as f64 / samples as f64).unwrap()
259    }
260}
261
262impl<T: Float> Default for ParetoFront<T> {
263    fn default() -> Self {
264        Self::new()
265    }
266}
267
268/// Results from multi-objective optimization
269#[derive(Clone, Debug)]
270pub struct MultiObjectiveResult<T: Float> {
271    /// Final Pareto front
272    pub pareto_front: ParetoFront<T>,
273    /// Number of generations performed
274    pub generations: usize,
275    /// Final population
276    pub final_population: Vec<Individual<T>>,
277    /// Convergence status
278    pub converged: bool,
279    /// Evolution history (optional)
280    pub evolution_history: Option<Vec<ParetoFront<T>>>,
281}
282
283/// Trait for multi-objective function evaluation
284pub trait MultiObjectiveFunction<T: Float> {
285    /// Number of objectives
286    fn num_objectives(&self) -> usize;
287
288    /// Number of decision variables
289    fn num_variables(&self) -> usize;
290
291    /// Evaluate all objectives for given variables
292    fn evaluate(&self, variables: &[T]) -> Vec<T>;
293
294    /// Evaluate constraints (return violations, ≤ 0 means feasible)
295    fn evaluate_constraints(&self, _variables: &[T]) -> Vec<T> {
296        Vec::new() // Default: no constraints
297    }
298
299    /// Get variable bounds (min, max) for each variable
300    fn variable_bounds(&self) -> Vec<(T, T)>;
301
302    /// Get ideal point (best possible values for each objective)
303    fn ideal_point(&self) -> Option<Vec<T>> {
304        None
305    }
306
307    /// Get nadir point (worst values in current Pareto set)
308    fn nadir_point(&self) -> Option<Vec<T>> {
309        None
310    }
311}
312
313/// NSGA-II optimizer for multi-objective problems
314#[derive(Clone, Debug)]
315pub struct NsgaII<T: Float> {
316    config: MultiObjectiveConfig<T>,
317    _phantom: PhantomData<T>,
318}
319
320impl<T: Float> NsgaII<T> {
321    /// Create new NSGA-II optimizer
322    pub fn new(config: MultiObjectiveConfig<T>) -> Self {
323        Self {
324            config,
325            _phantom: PhantomData,
326        }
327    }
328
329    /// Create optimizer with default configuration
330    pub fn with_default_config() -> Self {
331        Self::new(MultiObjectiveConfig::default())
332    }
333
334    /// Optimize multi-objective problem
335    pub fn optimize<
336        const DIM: usize,
337        C: crate::phantom::ConstraintState,
338        V: crate::phantom::ConvexityState,
339        M: crate::phantom::ManifoldState,
340    >(
341        &self,
342        _problem: &OptimizationProblem<DIM, C, MultiObjective, V, M>,
343        objective_function: &impl MultiObjectiveFunction<T>,
344    ) -> OptimizationResult<MultiObjectiveResult<T>> {
345        let mut rng = thread_rng();
346
347        // Initialize population
348        let mut population = self.initialize_population(objective_function, &mut rng)?;
349
350        // Evaluate initial population
351        for individual in &mut population {
352            individual.objectives = objective_function.evaluate(&individual.variables);
353            individual.constraint_violations =
354                objective_function.evaluate_constraints(&individual.variables);
355        }
356
357        let mut evolution_history = if self.config.max_generations < 100 {
358            Some(Vec::with_capacity(self.config.max_generations))
359        } else {
360            None
361        };
362
363        let mut best_hypervolume = T::zero();
364        let mut stagnation_count = 0;
365
366        for generation in 0..self.config.max_generations {
367            // Non-dominated sorting
368            let fronts = self.non_dominated_sort(&population);
369
370            // Calculate crowding distances
371            let mut ranked_population = Vec::new();
372            for front in &fronts {
373                let mut front_with_distance = front.clone();
374                self.calculate_crowding_distance(&mut front_with_distance);
375                ranked_population.extend(front_with_distance);
376            }
377
378            // Generate offspring through selection, crossover, and mutation
379            let offspring =
380                self.generate_offspring(&ranked_population, objective_function, &mut rng)?;
381
382            // Combine parent and offspring populations
383            population.extend(offspring);
384
385            // Environmental selection (select best individuals)
386            population = self.environmental_selection(population);
387
388            // Calculate current Pareto front and hypervolume
389            let mut current_front = self.extract_pareto_front(&population);
390            let current_hypervolume = if let Some(ref_point) = &self.config.reference_point {
391                current_front.calculate_hypervolume(ref_point)
392            } else {
393                T::zero()
394            };
395
396            // Check for convergence
397            if (current_hypervolume - best_hypervolume).abs() < self.config.convergence_tolerance {
398                stagnation_count += 1;
399            } else {
400                stagnation_count = 0;
401                best_hypervolume = current_hypervolume;
402            }
403
404            if let Some(ref mut history) = evolution_history {
405                history.push(current_front);
406            }
407
408            // Early termination if converged (more conservative for diversity)
409            if stagnation_count >= 100 {
410                let final_front = self.extract_pareto_front(&population);
411                return Ok(MultiObjectiveResult {
412                    pareto_front: final_front,
413                    generations: generation + 1,
414                    final_population: population,
415                    converged: true,
416                    evolution_history,
417                });
418            }
419        }
420
421        // Final Pareto front extraction
422        let final_front = self.extract_pareto_front(&population);
423
424        Ok(MultiObjectiveResult {
425            pareto_front: final_front,
426            generations: self.config.max_generations,
427            final_population: population,
428            converged: true, // Consider successfully completing all generations as converged
429            evolution_history,
430        })
431    }
432
433    /// Initialize random population
434    fn initialize_population(
435        &self,
436        objective_function: &impl MultiObjectiveFunction<T>,
437        rng: &mut ThreadRng,
438    ) -> OptimizationResult<Vec<Individual<T>>> {
439        let bounds = objective_function.variable_bounds();
440        let mut population = Vec::with_capacity(self.config.population_size);
441
442        for _ in 0..self.config.population_size {
443            let variables: Vec<T> = bounds
444                .iter()
445                .map(|(min, max)| {
446                    let range = *max - *min;
447                    *min + T::from(rng.gen::<f64>()).unwrap() * range
448                })
449                .collect();
450
451            population.push(Individual::new(variables));
452        }
453
454        Ok(population)
455    }
456
457    /// Perform non-dominated sorting
458    fn non_dominated_sort(&self, population: &[Individual<T>]) -> Vec<Vec<Individual<T>>> {
459        let mut fronts: Vec<Vec<Individual<T>>> = Vec::new();
460        let mut remaining = population.to_vec();
461
462        while !remaining.is_empty() {
463            let mut current_front = Vec::new();
464            let mut next_remaining = Vec::new();
465
466            for individual in &remaining {
467                let mut is_dominated = false;
468                for other in &remaining {
469                    if other.dominates(individual) {
470                        is_dominated = true;
471                        break;
472                    }
473                }
474
475                if !is_dominated {
476                    current_front.push(individual.clone());
477                } else {
478                    next_remaining.push(individual.clone());
479                }
480            }
481
482            if current_front.is_empty() {
483                break;
484            }
485
486            fronts.push(current_front);
487            remaining = next_remaining;
488        }
489
490        fronts
491    }
492
493    /// Calculate crowding distance for diversity preservation
494    fn calculate_crowding_distance(&self, front: &mut [Individual<T>]) {
495        if front.len() <= 2 {
496            for individual in front {
497                individual.crowding_distance = T::infinity();
498            }
499            return;
500        }
501
502        let num_objectives = front[0].objectives.len();
503
504        // Initialize crowding distances
505        for individual in front.iter_mut() {
506            individual.crowding_distance = T::zero();
507        }
508
509        for obj_idx in 0..num_objectives {
510            // Sort by current objective
511            front.sort_by(|a, b| {
512                a.objectives[obj_idx]
513                    .partial_cmp(&b.objectives[obj_idx])
514                    .unwrap_or(Ordering::Equal)
515            });
516
517            // Set boundary points to infinite distance
518            front[0].crowding_distance = T::infinity();
519            front[front.len() - 1].crowding_distance = T::infinity();
520
521            let obj_range =
522                front[front.len() - 1].objectives[obj_idx] - front[0].objectives[obj_idx];
523            if obj_range > T::zero() {
524                for i in 1..front.len() - 1 {
525                    let distance = (front[i + 1].objectives[obj_idx]
526                        - front[i - 1].objectives[obj_idx])
527                        / obj_range;
528                    front[i].crowding_distance = front[i].crowding_distance + distance;
529                }
530            }
531        }
532    }
533
534    /// Generate offspring through selection, crossover, and mutation
535    fn generate_offspring(
536        &self,
537        population: &[Individual<T>],
538        objective_function: &impl MultiObjectiveFunction<T>,
539        rng: &mut ThreadRng,
540    ) -> OptimizationResult<Vec<Individual<T>>> {
541        let mut offspring = Vec::with_capacity(self.config.population_size);
542
543        while offspring.len() < self.config.population_size {
544            // Tournament selection
545            let parent1 = self.tournament_selection(population, rng);
546            let parent2 = self.tournament_selection(population, rng);
547
548            // Crossover
549            let (mut child1, mut child2) =
550                if rng.gen::<f64>() < self.config.crossover_probability.to_f64().unwrap() {
551                    self.simulated_binary_crossover(parent1, parent2, rng)
552                } else {
553                    (parent1.clone(), parent2.clone())
554                };
555
556            // Mutation
557            if rng.gen::<f64>() < self.config.mutation_probability.to_f64().unwrap() {
558                self.polynomial_mutation(&mut child1, objective_function, rng);
559            }
560            if rng.gen::<f64>() < self.config.mutation_probability.to_f64().unwrap() {
561                self.polynomial_mutation(&mut child2, objective_function, rng);
562            }
563
564            // Evaluate offspring
565            child1.objectives = objective_function.evaluate(&child1.variables);
566            child1.constraint_violations =
567                objective_function.evaluate_constraints(&child1.variables);
568            child2.objectives = objective_function.evaluate(&child2.variables);
569            child2.constraint_violations =
570                objective_function.evaluate_constraints(&child2.variables);
571
572            offspring.push(child1);
573            if offspring.len() < self.config.population_size {
574                offspring.push(child2);
575            }
576        }
577
578        Ok(offspring)
579    }
580
581    /// Tournament selection
582    fn tournament_selection<'a>(
583        &self,
584        population: &'a [Individual<T>],
585        rng: &mut ThreadRng,
586    ) -> &'a Individual<T> {
587        let tournament_size = 2;
588        let mut best = &population[rng.gen_range(0..population.len())];
589
590        for _ in 1..tournament_size {
591            let candidate = &population[rng.gen_range(0..population.len())];
592            if candidate.rank < best.rank
593                || (candidate.rank == best.rank
594                    && candidate.crowding_distance > best.crowding_distance)
595            {
596                best = candidate;
597            }
598        }
599
600        best
601    }
602
603    /// Simulated binary crossover
604    fn simulated_binary_crossover(
605        &self,
606        parent1: &Individual<T>,
607        parent2: &Individual<T>,
608        rng: &mut ThreadRng,
609    ) -> (Individual<T>, Individual<T>) {
610        let eta_c = 20.0; // Distribution index
611        let mut child1 = parent1.clone();
612        let mut child2 = parent2.clone();
613
614        for i in 0..parent1.variables.len() {
615            if rng.gen::<f64>() <= 0.5 {
616                let p1 = parent1.variables[i].to_f64().unwrap();
617                let p2 = parent2.variables[i].to_f64().unwrap();
618
619                let u = rng.gen::<f64>();
620                let beta = if u <= 0.5 {
621                    (2.0 * u).powf(1.0 / (eta_c + 1.0))
622                } else {
623                    (1.0 / (2.0 * (1.0 - u))).powf(1.0 / (eta_c + 1.0))
624                };
625
626                let c1 = 0.5 * ((1.0 + beta) * p1 + (1.0 - beta) * p2);
627                let c2 = 0.5 * ((1.0 - beta) * p1 + (1.0 + beta) * p2);
628
629                child1.variables[i] = T::from(c1).unwrap();
630                child2.variables[i] = T::from(c2).unwrap();
631            }
632        }
633
634        (child1, child2)
635    }
636
637    /// Polynomial mutation
638    fn polynomial_mutation(
639        &self,
640        individual: &mut Individual<T>,
641        objective_function: &impl MultiObjectiveFunction<T>,
642        rng: &mut ThreadRng,
643    ) {
644        let bounds = objective_function.variable_bounds();
645        let eta_m = 20.0; // Distribution index
646        let num_variables = individual.variables.len();
647
648        for (i, variable) in individual.variables.iter_mut().enumerate() {
649            if rng.gen::<f64>() <= (1.0 / num_variables as f64) {
650                let (lower, upper) = bounds[i];
651                let x = variable.to_f64().unwrap();
652                let xl = lower.to_f64().unwrap();
653                let xu = upper.to_f64().unwrap();
654
655                let delta1 = (x - xl) / (xu - xl);
656                let delta2 = (xu - x) / (xu - xl);
657
658                let rnd = rng.gen::<f64>();
659                let deltaq = if rnd <= 0.5 {
660                    let xy = 1.0 - delta1;
661                    let val = 2.0 * rnd + (1.0 - 2.0 * rnd) * xy.powf(eta_m + 1.0);
662                    val.powf(1.0 / (eta_m + 1.0)) - 1.0
663                } else {
664                    let xy = 1.0 - delta2;
665                    let val = 2.0 * (1.0 - rnd) + 2.0 * (rnd - 0.5) * xy.powf(eta_m + 1.0);
666                    1.0 - val.powf(1.0 / (eta_m + 1.0))
667                };
668
669                let new_x = x + deltaq * (xu - xl);
670                *variable = T::from(new_x.max(xl).min(xu)).unwrap();
671            }
672        }
673    }
674
675    /// Environmental selection (NSGA-II selection)
676    fn environmental_selection(&self, population: Vec<Individual<T>>) -> Vec<Individual<T>> {
677        if population.len() <= self.config.population_size {
678            return population;
679        }
680
681        // Non-dominated sorting
682        let fronts = self.non_dominated_sort(&population);
683        let mut selected = Vec::new();
684
685        for mut front in fronts {
686            if selected.len() + front.len() <= self.config.population_size {
687                // Include entire front
688                selected.extend(front);
689            } else {
690                // Include part of front based on crowding distance
691                self.calculate_crowding_distance(&mut front);
692                front.sort_by(|a, b| {
693                    b.crowding_distance
694                        .partial_cmp(&a.crowding_distance)
695                        .unwrap_or(Ordering::Equal)
696                });
697
698                let remaining_slots = self.config.population_size - selected.len();
699                selected.extend(front.into_iter().take(remaining_slots));
700                break;
701            }
702        }
703
704        selected
705    }
706
707    /// Extract current Pareto front from population
708    fn extract_pareto_front(&self, population: &[Individual<T>]) -> ParetoFront<T> {
709        let fronts = self.non_dominated_sort(population);
710        let mut pareto_front = ParetoFront::new();
711
712        if let Some(first_front) = fronts.first() {
713            pareto_front.solutions = first_front.clone();
714        }
715
716        if let Some(ref_point) = &self.config.reference_point {
717            pareto_front.hypervolume = Some(pareto_front.calculate_hypervolume(ref_point));
718            pareto_front.reference_point = Some(ref_point.clone());
719        }
720
721        pareto_front
722    }
723}
724
725#[cfg(test)]
726mod tests {
727    use super::*;
728
729    /// Test problem: ZDT1 (Zitzler-Deb-Thiele test problem 1)
730    struct ZDT1 {
731        num_variables: usize,
732    }
733
734    impl MultiObjectiveFunction<f64> for ZDT1 {
735        fn num_objectives(&self) -> usize {
736            2
737        }
738
739        fn num_variables(&self) -> usize {
740            self.num_variables
741        }
742
743        fn evaluate(&self, variables: &[f64]) -> Vec<f64> {
744            let f1 = variables[0];
745            let g =
746                1.0 + 9.0 * variables[1..].iter().sum::<f64>() / (self.num_variables - 1) as f64;
747            let h = 1.0 - (f1 / g).sqrt();
748            let f2 = g * h;
749
750            vec![f1, f2]
751        }
752
753        fn variable_bounds(&self) -> Vec<(f64, f64)> {
754            vec![(0.0, 1.0); self.num_variables]
755        }
756
757        fn ideal_point(&self) -> Option<Vec<f64>> {
758            Some(vec![0.0, 0.0])
759        }
760    }
761
762    #[test]
763    fn test_individual_dominance() {
764        let mut ind1 = Individual::new(vec![1.0, 2.0]);
765        ind1.objectives = vec![0.5, 0.3];
766
767        let mut ind2 = Individual::new(vec![1.5, 1.8]);
768        ind2.objectives = vec![0.7, 0.4];
769
770        // ind1 dominates ind2 (better in both objectives)
771        assert!(ind1.dominates(&ind2));
772        assert!(!ind2.dominates(&ind1));
773    }
774
775    #[test]
776    fn test_pareto_front_operations() {
777        let mut front = ParetoFront::new();
778
779        let mut sol1 = Individual::new(vec![1.0]);
780        sol1.objectives = vec![0.2, 0.8];
781
782        let mut sol2 = Individual::new(vec![2.0]);
783        sol2.objectives = vec![0.8, 0.2];
784
785        let mut sol3 = Individual::new(vec![3.0]);
786        sol3.objectives = vec![0.9, 0.9]; // Clearly dominated by both sol1 and sol2
787
788        assert!(front.add_if_non_dominated(sol1));
789        assert!(front.add_if_non_dominated(sol2));
790        assert!(!front.add_if_non_dominated(sol3)); // Should be dominated
791
792        assert_eq!(front.solutions.len(), 2);
793    }
794
795    #[test]
796    fn test_hypervolume_2d() {
797        let mut front = ParetoFront::new();
798
799        let mut sol1 = Individual::new(vec![1.0]);
800        sol1.objectives = vec![0.2, 0.8];
801
802        let mut sol2 = Individual::new(vec![2.0]);
803        sol2.objectives = vec![0.8, 0.2];
804
805        front.solutions = vec![sol1, sol2];
806
807        let reference_point = vec![1.0, 1.0];
808        let hypervolume = front.calculate_hypervolume(&reference_point);
809
810        // Should be positive for non-dominated solutions
811        assert!(hypervolume > 0.0);
812    }
813
814    #[test]
815    fn test_nsga2_basic_functionality() {
816        let zdt1 = ZDT1 { num_variables: 3 };
817        let config = MultiObjectiveConfig {
818            population_size: 20,
819            max_generations: 10,
820            crossover_probability: 0.9,
821            mutation_probability: 0.1,
822            mutation_strength: 0.1,
823            elite_ratio: 0.1,
824            convergence_tolerance: 1e-6,
825            reference_point: Some(vec![1.0, 1.0]),
826            preserve_diversity: true,
827        };
828
829        let optimizer = NsgaII::new(config);
830
831        // Use phantom type for multi-objective problem
832        use crate::phantom::{Euclidean, NonConvex, Unconstrained};
833        let problem: OptimizationProblem<3, Unconstrained, MultiObjective, NonConvex, Euclidean> =
834            OptimizationProblem::new();
835
836        let result = optimizer.optimize(&problem, &zdt1);
837
838        assert!(result.is_ok());
839        let solution = result.unwrap();
840
841        // Should have found some non-dominated solutions
842        assert!(!solution.pareto_front.solutions.is_empty());
843        assert!(solution.generations <= 10);
844    }
845
846    #[test]
847    fn test_non_dominated_sorting() {
848        let optimizer = NsgaII::<f64>::with_default_config();
849
850        let mut pop = Vec::new();
851
852        // Front 0 (non-dominated)
853        let mut ind1 = Individual::new(vec![1.0]);
854        ind1.objectives = vec![0.1, 0.9];
855        pop.push(ind1);
856
857        let mut ind2 = Individual::new(vec![2.0]);
858        ind2.objectives = vec![0.9, 0.1];
859        pop.push(ind2);
860
861        // Front 1 (dominated by front 0)
862        let mut ind3 = Individual::new(vec![3.0]);
863        ind3.objectives = vec![0.2, 0.95]; // Dominated by ind1 [0.1, 0.9]
864        pop.push(ind3);
865
866        let fronts = optimizer.non_dominated_sort(&pop);
867
868        assert_eq!(fronts.len(), 2);
869        assert_eq!(fronts[0].len(), 2); // Two non-dominated solutions
870        assert_eq!(fronts[1].len(), 1); // One dominated solution
871    }
872
873    #[test]
874    fn test_crowding_distance_calculation() {
875        let optimizer = NsgaII::<f64>::with_default_config();
876
877        let mut front = vec![
878            {
879                let mut ind = Individual::new(vec![1.0]);
880                ind.objectives = vec![0.1, 0.9];
881                ind
882            },
883            {
884                let mut ind = Individual::new(vec![2.0]);
885                ind.objectives = vec![0.5, 0.5];
886                ind
887            },
888            {
889                let mut ind = Individual::new(vec![3.0]);
890                ind.objectives = vec![0.9, 0.1];
891                ind
892            },
893        ];
894
895        optimizer.calculate_crowding_distance(&mut front);
896
897        // Boundary solutions should have infinite crowding distance
898        assert!(
899            front[0].crowding_distance.is_infinite() || front[2].crowding_distance.is_infinite()
900        );
901
902        // Middle solution should have finite crowding distance
903        assert!(front[1].crowding_distance.is_finite() && front[1].crowding_distance > 0.0);
904    }
905}