Skip to main content

fugue_evo/algorithms/
nsga2.rs

1//! NSGA-II (Non-dominated Sorting Genetic Algorithm II)
2//!
3//! Implements the NSGA-II algorithm for multi-objective optimization.
4//!
5//! Reference: Deb, K., Pratap, A., Agarwal, S., & Meyarivan, T. (2002).
6//! A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II.
7//! IEEE Transactions on Evolutionary Computation, 6(2).
8
9use std::marker::PhantomData;
10
11use rand::Rng;
12
13use crate::error::EvoResult;
14use crate::fitness::traits::ParetoFitness;
15use crate::genome::bounds::MultiBounds;
16use crate::genome::traits::EvolutionaryGenome;
17use crate::operators::traits::{
18    BoundedCrossoverOperator, BoundedMutationOperator, CrossoverOperator, MutationOperator,
19};
20use crate::population::individual::Individual;
21
22/// Multi-objective fitness function trait
23#[cfg(feature = "parallel")]
24pub trait MultiObjectiveFitness<G>: Send + Sync {
25    /// Number of objectives
26    fn num_objectives(&self) -> usize;
27
28    /// Evaluate all objectives (all to be minimized by convention)
29    fn evaluate(&self, genome: &G) -> Vec<f64>;
30}
31
32/// Multi-objective fitness function trait
33#[cfg(not(feature = "parallel"))]
34pub trait MultiObjectiveFitness<G> {
35    /// Number of objectives
36    fn num_objectives(&self) -> usize;
37
38    /// Evaluate all objectives (all to be minimized by convention)
39    fn evaluate(&self, genome: &G) -> Vec<f64>;
40}
41
42/// Implement MultiObjectiveFitness for closures
43#[cfg(feature = "parallel")]
44impl<G, F> MultiObjectiveFitness<G> for F
45where
46    F: Fn(&G) -> Vec<f64> + Send + Sync,
47{
48    fn num_objectives(&self) -> usize {
49        // This is a limitation - closure doesn't know number of objectives
50        // Users should use the explicit trait implementation for better type safety
51        2 // Default assumption
52    }
53
54    fn evaluate(&self, genome: &G) -> Vec<f64> {
55        self(genome)
56    }
57}
58
59/// Implement MultiObjectiveFitness for closures
60#[cfg(not(feature = "parallel"))]
61impl<G, F> MultiObjectiveFitness<G> for F
62where
63    F: Fn(&G) -> Vec<f64>,
64{
65    fn num_objectives(&self) -> usize {
66        2 // Default assumption
67    }
68
69    fn evaluate(&self, genome: &G) -> Vec<f64> {
70        self(genome)
71    }
72}
73
74/// NSGA-II individual with objectives and crowding info
75#[derive(Clone, Debug)]
76pub struct Nsga2Individual<G: EvolutionaryGenome> {
77    /// The genome
78    pub genome: G,
79    /// Objective values
80    pub objectives: Vec<f64>,
81    /// Pareto rank (0 = first front)
82    pub rank: usize,
83    /// Crowding distance
84    pub crowding_distance: f64,
85}
86
87impl<G: EvolutionaryGenome> Nsga2Individual<G> {
88    /// Create a new individual with evaluated objectives
89    pub fn new(genome: G, objectives: Vec<f64>) -> Self {
90        Self {
91            genome,
92            objectives,
93            rank: usize::MAX,
94            crowding_distance: 0.0,
95        }
96    }
97
98    /// Check if this individual dominates another
99    /// (all objectives <= and at least one <, since we minimize)
100    pub fn dominates(&self, other: &Self) -> bool {
101        let at_least_as_good = self
102            .objectives
103            .iter()
104            .zip(other.objectives.iter())
105            .all(|(a, b)| a <= b);
106        let strictly_better = self
107            .objectives
108            .iter()
109            .zip(other.objectives.iter())
110            .any(|(a, b)| a < b);
111        at_least_as_good && strictly_better
112    }
113
114    /// Convert to ParetoFitness
115    pub fn to_pareto_fitness(&self) -> ParetoFitness {
116        let mut pf = ParetoFitness::new(self.objectives.clone());
117        pf.rank = self.rank;
118        pf.crowding_distance = self.crowding_distance;
119        pf
120    }
121
122    /// Convert to Individual with ParetoFitness
123    pub fn to_individual(self) -> Individual<G, ParetoFitness> {
124        let fitness = self.to_pareto_fitness();
125        Individual::with_fitness(self.genome, fitness)
126    }
127}
128
129/// Fast non-dominated sort
130///
131/// Returns fronts where `front[0]` is the Pareto-optimal front
132pub fn fast_non_dominated_sort<G: EvolutionaryGenome>(
133    population: &mut [Nsga2Individual<G>],
134) -> Vec<Vec<usize>> {
135    let n = population.len();
136    if n == 0 {
137        return vec![];
138    }
139
140    // domination_count[i] = number of individuals that dominate i
141    let mut domination_count = vec![0usize; n];
142    // dominated_set[i] = set of individuals that i dominates
143    let mut dominated_set: Vec<Vec<usize>> = vec![vec![]; n];
144
145    // Calculate domination relationships
146    for i in 0..n {
147        for j in (i + 1)..n {
148            if population[i].dominates(&population[j]) {
149                dominated_set[i].push(j);
150                domination_count[j] += 1;
151            } else if population[j].dominates(&population[i]) {
152                dominated_set[j].push(i);
153                domination_count[i] += 1;
154            }
155        }
156    }
157
158    // Build fronts
159    let mut fronts: Vec<Vec<usize>> = vec![];
160    let mut current_front: Vec<usize> = (0..n).filter(|&i| domination_count[i] == 0).collect();
161
162    let mut rank = 0;
163    while !current_front.is_empty() {
164        // Assign rank to current front
165        for &i in &current_front {
166            population[i].rank = rank;
167        }
168
169        // Build next front
170        let mut next_front = vec![];
171        for &i in &current_front {
172            for &j in &dominated_set[i] {
173                domination_count[j] -= 1;
174                if domination_count[j] == 0 {
175                    next_front.push(j);
176                }
177            }
178        }
179
180        fronts.push(current_front);
181        current_front = next_front;
182        rank += 1;
183    }
184
185    fronts
186}
187
188/// Calculate crowding distance for a front
189pub fn calculate_crowding_distance<G: EvolutionaryGenome>(
190    population: &mut [Nsga2Individual<G>],
191    front: &[usize],
192) {
193    let n = front.len();
194    if n <= 2 {
195        for &i in front {
196            population[i].crowding_distance = f64::INFINITY;
197        }
198        return;
199    }
200
201    // Reset distances
202    for &i in front {
203        population[i].crowding_distance = 0.0;
204    }
205
206    let num_objectives = population[front[0]].objectives.len();
207
208    for obj in 0..num_objectives {
209        // Sort front by this objective
210        let mut sorted_indices: Vec<usize> = front.to_vec();
211        sorted_indices.sort_by(|&a, &b| {
212            population[a].objectives[obj]
213                .partial_cmp(&population[b].objectives[obj])
214                .unwrap_or(std::cmp::Ordering::Equal)
215        });
216
217        // Boundary individuals get infinite distance
218        population[sorted_indices[0]].crowding_distance = f64::INFINITY;
219        population[sorted_indices[n - 1]].crowding_distance = f64::INFINITY;
220
221        // Calculate range
222        let obj_min = population[sorted_indices[0]].objectives[obj];
223        let obj_max = population[sorted_indices[n - 1]].objectives[obj];
224        let obj_range = obj_max - obj_min;
225
226        if obj_range > 0.0 {
227            for i in 1..(n - 1) {
228                let idx = sorted_indices[i];
229                let prev_val = population[sorted_indices[i - 1]].objectives[obj];
230                let next_val = population[sorted_indices[i + 1]].objectives[obj];
231                population[idx].crowding_distance += (next_val - prev_val) / obj_range;
232            }
233        }
234    }
235}
236
237/// Crowded comparison operator
238///
239/// Returns true if a is better than b (lower rank, or same rank with higher crowding distance)
240pub fn crowded_comparison<G: EvolutionaryGenome>(
241    a: &Nsga2Individual<G>,
242    b: &Nsga2Individual<G>,
243) -> bool {
244    a.rank < b.rank || (a.rank == b.rank && a.crowding_distance > b.crowding_distance)
245}
246
247/// NSGA-II algorithm
248pub struct Nsga2<G, F, C, M> {
249    /// Population size
250    pub population_size: usize,
251    /// Crossover probability
252    pub crossover_probability: f64,
253    /// Mutation probability
254    pub mutation_probability: f64,
255    /// Problem bounds
256    pub bounds: Option<MultiBounds>,
257    /// Marker for types
258    _phantom: PhantomData<(G, F, C, M)>,
259}
260
261impl<G, F, C, M> Nsga2<G, F, C, M>
262where
263    G: EvolutionaryGenome,
264    F: MultiObjectiveFitness<G>,
265    C: CrossoverOperator<G>,
266    M: MutationOperator<G>,
267{
268    /// Create a new NSGA-II algorithm
269    pub fn new(population_size: usize) -> Self {
270        Self {
271            population_size,
272            crossover_probability: 0.9,
273            mutation_probability: 1.0,
274            bounds: None,
275            _phantom: PhantomData,
276        }
277    }
278
279    /// Set crossover probability
280    pub fn with_crossover_probability(mut self, prob: f64) -> Self {
281        self.crossover_probability = prob;
282        self
283    }
284
285    /// Set mutation probability
286    pub fn with_mutation_probability(mut self, prob: f64) -> Self {
287        self.mutation_probability = prob;
288        self
289    }
290
291    /// Set bounds
292    pub fn with_bounds(mut self, bounds: MultiBounds) -> Self {
293        self.bounds = Some(bounds);
294        self
295    }
296
297    /// Initialize random population
298    pub fn initialize_population<R: Rng>(
299        &self,
300        fitness: &F,
301        bounds: &MultiBounds,
302        rng: &mut R,
303    ) -> Vec<Nsga2Individual<G>> {
304        (0..self.population_size)
305            .map(|_| {
306                let genome = G::generate(rng, bounds);
307                let objectives = fitness.evaluate(&genome);
308                Nsga2Individual::new(genome, objectives)
309            })
310            .collect()
311    }
312
313    /// Binary tournament selection with crowded comparison
314    pub fn tournament_select<'a, R: Rng>(
315        &self,
316        population: &'a [Nsga2Individual<G>],
317        rng: &mut R,
318    ) -> &'a Nsga2Individual<G> {
319        let i = rng.gen_range(0..population.len());
320        let j = rng.gen_range(0..population.len());
321
322        if crowded_comparison(&population[i], &population[j]) {
323            &population[i]
324        } else {
325            &population[j]
326        }
327    }
328
329    /// Create offspring population
330    pub fn create_offspring<R: Rng>(
331        &self,
332        population: &[Nsga2Individual<G>],
333        fitness: &F,
334        crossover: &C,
335        mutation: &M,
336        rng: &mut R,
337    ) -> Vec<Nsga2Individual<G>> {
338        let mut offspring = Vec::with_capacity(self.population_size);
339
340        while offspring.len() < self.population_size {
341            // Select parents
342            let parent1 = self.tournament_select(population, rng);
343            let parent2 = self.tournament_select(population, rng);
344
345            // Crossover
346            let (mut child1, mut child2) = if rng.gen::<f64>() < self.crossover_probability {
347                match crossover.crossover(&parent1.genome, &parent2.genome, rng) {
348                    crate::error::OperatorResult::Success((c1, c2)) => (c1, c2),
349                    _ => (parent1.genome.clone(), parent2.genome.clone()),
350                }
351            } else {
352                (parent1.genome.clone(), parent2.genome.clone())
353            };
354
355            // Mutation
356            if rng.gen::<f64>() < self.mutation_probability {
357                mutation.mutate(&mut child1, rng);
358            }
359            if rng.gen::<f64>() < self.mutation_probability {
360                mutation.mutate(&mut child2, rng);
361            }
362
363            // Evaluate
364            let obj1 = fitness.evaluate(&child1);
365            let obj2 = fitness.evaluate(&child2);
366
367            offspring.push(Nsga2Individual::new(child1, obj1));
368            if offspring.len() < self.population_size {
369                offspring.push(Nsga2Individual::new(child2, obj2));
370            }
371        }
372
373        offspring
374    }
375
376    /// Run one generation of NSGA-II
377    pub fn step<R: Rng>(
378        &self,
379        population: &mut Vec<Nsga2Individual<G>>,
380        fitness: &F,
381        crossover: &C,
382        mutation: &M,
383        rng: &mut R,
384    ) {
385        // Create offspring
386        let offspring = self.create_offspring(population, fitness, crossover, mutation, rng);
387
388        // Combine parent and offspring populations
389        let mut combined: Vec<Nsga2Individual<G>> =
390            population.drain(..).chain(offspring.into_iter()).collect();
391
392        // Non-dominated sort
393        let fronts = fast_non_dominated_sort(&mut combined);
394
395        // Fill new population from fronts
396        let mut new_pop = Vec::with_capacity(self.population_size);
397
398        for front in fronts {
399            if new_pop.len() + front.len() <= self.population_size {
400                // Add entire front
401                for &i in &front {
402                    new_pop.push(combined[i].clone());
403                }
404            } else {
405                // Partial front - sort by crowding distance
406                calculate_crowding_distance(&mut combined, &front);
407
408                let mut sorted_front: Vec<usize> = front.to_vec();
409                sorted_front.sort_by(|&a, &b| {
410                    combined[b]
411                        .crowding_distance
412                        .partial_cmp(&combined[a].crowding_distance)
413                        .unwrap_or(std::cmp::Ordering::Equal)
414                });
415
416                let remaining = self.population_size - new_pop.len();
417                for &i in sorted_front.iter().take(remaining) {
418                    new_pop.push(combined[i].clone());
419                }
420                break;
421            }
422        }
423
424        // Update crowding distance for the entire new population
425        let all_indices: Vec<usize> = (0..new_pop.len()).collect();
426        calculate_crowding_distance(&mut new_pop, &all_indices);
427
428        *population = new_pop;
429    }
430
431    /// Run NSGA-II for a fixed number of generations
432    pub fn run<R: Rng>(
433        &self,
434        fitness: &F,
435        crossover: &C,
436        mutation: &M,
437        bounds: &MultiBounds,
438        max_generations: usize,
439        rng: &mut R,
440    ) -> EvoResult<Vec<Nsga2Individual<G>>> {
441        let mut population = self.initialize_population(fitness, bounds, rng);
442
443        // Initial non-dominated sort
444        fast_non_dominated_sort(&mut population);
445        let all_indices: Vec<usize> = (0..population.len()).collect();
446        calculate_crowding_distance(&mut population, &all_indices);
447
448        for _ in 0..max_generations {
449            self.step(&mut population, fitness, crossover, mutation, rng);
450        }
451
452        Ok(population)
453    }
454
455    /// Get the Pareto front (rank 0 individuals)
456    pub fn get_pareto_front(population: &[Nsga2Individual<G>]) -> Vec<&Nsga2Individual<G>> {
457        population.iter().filter(|ind| ind.rank == 0).collect()
458    }
459}
460
461/// Version with bounded operators
462impl<G, F, C, M> Nsga2<G, F, C, M>
463where
464    G: EvolutionaryGenome,
465    F: MultiObjectiveFitness<G>,
466    C: BoundedCrossoverOperator<G>,
467    M: BoundedMutationOperator<G>,
468{
469    /// Create offspring population with bounded operators
470    pub fn create_offspring_bounded<R: Rng>(
471        &self,
472        population: &[Nsga2Individual<G>],
473        fitness: &F,
474        crossover: &C,
475        mutation: &M,
476        bounds: &MultiBounds,
477        rng: &mut R,
478    ) -> Vec<Nsga2Individual<G>> {
479        let mut offspring = Vec::with_capacity(self.population_size);
480
481        while offspring.len() < self.population_size {
482            let parent1 = self.tournament_select(population, rng);
483            let parent2 = self.tournament_select(population, rng);
484
485            let (mut child1, mut child2) = if rng.gen::<f64>() < self.crossover_probability {
486                match crossover.crossover_bounded(&parent1.genome, &parent2.genome, bounds, rng) {
487                    crate::error::OperatorResult::Success((c1, c2)) => (c1, c2),
488                    _ => (parent1.genome.clone(), parent2.genome.clone()),
489                }
490            } else {
491                (parent1.genome.clone(), parent2.genome.clone())
492            };
493
494            if rng.gen::<f64>() < self.mutation_probability {
495                mutation.mutate_bounded(&mut child1, bounds, rng);
496            }
497            if rng.gen::<f64>() < self.mutation_probability {
498                mutation.mutate_bounded(&mut child2, bounds, rng);
499            }
500
501            let obj1 = fitness.evaluate(&child1);
502            let obj2 = fitness.evaluate(&child2);
503
504            offspring.push(Nsga2Individual::new(child1, obj1));
505            if offspring.len() < self.population_size {
506                offspring.push(Nsga2Individual::new(child2, obj2));
507            }
508        }
509
510        offspring
511    }
512
513    /// Run one generation with bounded operators
514    pub fn step_bounded<R: Rng>(
515        &self,
516        population: &mut Vec<Nsga2Individual<G>>,
517        fitness: &F,
518        crossover: &C,
519        mutation: &M,
520        bounds: &MultiBounds,
521        rng: &mut R,
522    ) {
523        let offspring =
524            self.create_offspring_bounded(population, fitness, crossover, mutation, bounds, rng);
525
526        let mut combined: Vec<Nsga2Individual<G>> =
527            population.drain(..).chain(offspring.into_iter()).collect();
528
529        let fronts = fast_non_dominated_sort(&mut combined);
530
531        let mut new_pop = Vec::with_capacity(self.population_size);
532
533        for front in fronts {
534            if new_pop.len() + front.len() <= self.population_size {
535                for &i in &front {
536                    new_pop.push(combined[i].clone());
537                }
538            } else {
539                calculate_crowding_distance(&mut combined, &front);
540
541                let mut sorted_front: Vec<usize> = front.to_vec();
542                sorted_front.sort_by(|&a, &b| {
543                    combined[b]
544                        .crowding_distance
545                        .partial_cmp(&combined[a].crowding_distance)
546                        .unwrap_or(std::cmp::Ordering::Equal)
547                });
548
549                let remaining = self.population_size - new_pop.len();
550                for &i in sorted_front.iter().take(remaining) {
551                    new_pop.push(combined[i].clone());
552                }
553                break;
554            }
555        }
556
557        let all_indices: Vec<usize> = (0..new_pop.len()).collect();
558        calculate_crowding_distance(&mut new_pop, &all_indices);
559
560        *population = new_pop;
561    }
562
563    /// Run NSGA-II with bounded operators
564    pub fn run_bounded<R: Rng>(
565        &self,
566        fitness: &F,
567        crossover: &C,
568        mutation: &M,
569        bounds: &MultiBounds,
570        max_generations: usize,
571        rng: &mut R,
572    ) -> EvoResult<Vec<Nsga2Individual<G>>> {
573        let mut population = self.initialize_population(fitness, bounds, rng);
574
575        fast_non_dominated_sort(&mut population);
576        let all_indices: Vec<usize> = (0..population.len()).collect();
577        calculate_crowding_distance(&mut population, &all_indices);
578
579        for _ in 0..max_generations {
580            self.step_bounded(&mut population, fitness, crossover, mutation, bounds, rng);
581        }
582
583        Ok(population)
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590    use crate::genome::real_vector::RealVector;
591    use crate::genome::traits::RealValuedGenome;
592    use crate::operators::crossover::SbxCrossover;
593    use crate::operators::mutation::PolynomialMutation;
594
595    // ZDT1 test problem
596    struct Zdt1;
597
598    impl MultiObjectiveFitness<RealVector> for Zdt1 {
599        fn num_objectives(&self) -> usize {
600            2
601        }
602
603        fn evaluate(&self, genome: &RealVector) -> Vec<f64> {
604            let x = genome.genes();
605            let n = x.len() as f64;
606
607            let f1 = x[0];
608
609            let g: f64 = 1.0 + 9.0 * x[1..].iter().sum::<f64>() / (n - 1.0);
610            let f2 = g * (1.0 - (f1 / g).sqrt());
611
612            vec![f1, f2]
613        }
614    }
615
616    #[test]
617    fn test_domination() {
618        let a = Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![1.0, 2.0]);
619        let b = Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![2.0, 3.0]);
620        let c = Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![1.5, 1.5]);
621
622        assert!(a.dominates(&b)); // a is better in both objectives
623        assert!(!b.dominates(&a));
624        assert!(!a.dominates(&c)); // c is better in second objective
625        assert!(!c.dominates(&a)); // a is better in first objective
626    }
627
628    #[test]
629    fn test_fast_non_dominated_sort() {
630        let mut population = vec![
631            Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![1.0, 4.0]),
632            Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![2.0, 3.0]),
633            Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![3.0, 2.0]),
634            Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![4.0, 1.0]),
635            Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![3.0, 3.0]),
636        ];
637
638        let fronts = fast_non_dominated_sort(&mut population);
639
640        // First 4 should be in front 0 (none dominates each other)
641        assert_eq!(fronts[0].len(), 4);
642        // Last one should be in front 1 (dominated by [2,3] and [3,2])
643        assert_eq!(fronts[1].len(), 1);
644
645        for &i in &fronts[0] {
646            assert_eq!(population[i].rank, 0);
647        }
648        for &i in &fronts[1] {
649            assert_eq!(population[i].rank, 1);
650        }
651    }
652
653    #[test]
654    fn test_crowding_distance() {
655        let mut population = vec![
656            Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![0.0, 10.0]),
657            Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![5.0, 5.0]),
658            Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![10.0, 0.0]),
659        ];
660
661        let front: Vec<usize> = (0..population.len()).collect();
662        calculate_crowding_distance(&mut population, &front);
663
664        // Boundary points should have infinite distance
665        assert!(population[0].crowding_distance.is_infinite());
666        assert!(population[2].crowding_distance.is_infinite());
667        // Middle point should have finite distance
668        assert!(population[1].crowding_distance.is_finite());
669        assert!(population[1].crowding_distance > 0.0);
670    }
671
672    #[test]
673    fn test_crowded_comparison() {
674        let mut a = Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![1.0, 1.0]);
675        a.rank = 0;
676        a.crowding_distance = 2.0;
677
678        let mut b = Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![2.0, 2.0]);
679        b.rank = 1;
680        b.crowding_distance = 3.0;
681
682        let mut c = Nsga2Individual::<RealVector>::new(RealVector::new(vec![0.0]), vec![1.5, 1.5]);
683        c.rank = 0;
684        c.crowding_distance = 1.0;
685
686        assert!(crowded_comparison(&a, &b)); // a has lower rank
687        assert!(crowded_comparison(&a, &c)); // same rank, a has higher crowding distance
688        assert!(!crowded_comparison(&c, &a)); // c has lower crowding distance
689    }
690
691    #[test]
692    fn test_nsga2_initialization() {
693        use crate::genome::bounds::Bounds;
694        let mut rng = rand::thread_rng();
695        let fitness = Zdt1;
696        let bounds = MultiBounds::new(vec![Bounds::new(0.0, 1.0); 10]);
697
698        let nsga2: Nsga2<RealVector, Zdt1, SbxCrossover, PolynomialMutation> = Nsga2::new(20);
699        let population = nsga2.initialize_population(&fitness, &bounds, &mut rng);
700
701        assert_eq!(population.len(), 20);
702        for ind in &population {
703            assert_eq!(ind.objectives.len(), 2);
704        }
705    }
706
707    #[test]
708    fn test_nsga2_run() {
709        use crate::genome::bounds::Bounds;
710        let mut rng = rand::thread_rng();
711        let fitness = Zdt1;
712        let bounds = MultiBounds::new(vec![Bounds::new(0.0, 1.0); 10]);
713        let crossover = SbxCrossover::new(15.0);
714        let mutation = PolynomialMutation::new(20.0);
715
716        let nsga2: Nsga2<RealVector, Zdt1, SbxCrossover, PolynomialMutation> = Nsga2::new(50);
717        let population = nsga2
718            .run_bounded(&fitness, &crossover, &mutation, &bounds, 10, &mut rng)
719            .unwrap();
720
721        assert_eq!(population.len(), 50);
722
723        // Check that we have a Pareto front
724        let pareto_front =
725            Nsga2::<RealVector, Zdt1, SbxCrossover, PolynomialMutation>::get_pareto_front(
726                &population,
727            );
728        assert!(!pareto_front.is_empty());
729    }
730
731    #[test]
732    fn test_nsga2_pareto_front_quality() {
733        use crate::genome::bounds::Bounds;
734        let mut rng = rand::thread_rng();
735        let fitness = Zdt1;
736        let bounds = MultiBounds::new(vec![Bounds::new(0.0, 1.0); 10]);
737        let crossover = SbxCrossover::new(15.0);
738        let mutation = PolynomialMutation::new(20.0);
739
740        let nsga2: Nsga2<RealVector, Zdt1, SbxCrossover, PolynomialMutation> = Nsga2::new(100);
741        let population = nsga2
742            .run_bounded(&fitness, &crossover, &mutation, &bounds, 50, &mut rng)
743            .unwrap();
744
745        let pareto_front =
746            Nsga2::<RealVector, Zdt1, SbxCrossover, PolynomialMutation>::get_pareto_front(
747                &population,
748            );
749
750        // Verify Pareto front properties
751        for ind in &pareto_front {
752            assert_eq!(ind.rank, 0);
753            // ZDT1 Pareto front has f2 = 1 - sqrt(f1)
754            // All objectives should be positive
755            assert!(ind.objectives[0] >= 0.0);
756            assert!(ind.objectives[1] >= 0.0);
757        }
758    }
759}