scirs2_optimize/
multi_objective.rs

1//! Multi-objective optimization algorithms
2//!
3//! This module provides algorithms for solving multi-objective optimization problems
4//! where multiple conflicting objectives need to be optimized simultaneously.
5
6use crate::error::OptimizeError;
7use ndarray::{Array1, Array2, ArrayView1};
8use rand::{prelude::*, rng};
9use std::cmp::Ordering;
10use std::collections::HashMap;
11
12/// Represents a solution in multi-objective optimization
13#[derive(Debug, Clone)]
14pub struct MultiObjectiveSolution {
15    /// Decision variables
16    pub variables: Array1<f64>,
17    /// Objective function values
18    pub objectives: Array1<f64>,
19    /// Constraint violation (0.0 if feasible)
20    pub constraint_violation: f64,
21    /// Rank in the population (for NSGA-II/III)
22    pub rank: usize,
23    /// Crowding distance (for NSGA-II)
24    pub crowding_distance: f64,
25    /// Additional metadata
26    pub metadata: HashMap<String, f64>,
27}
28
29/// Result of multi-objective optimization
30#[derive(Debug, Clone)]
31pub struct MultiObjectiveResult {
32    /// Pareto front solutions
33    pub pareto_front: Vec<MultiObjectiveSolution>,
34    /// All final population solutions
35    pub population: Vec<MultiObjectiveSolution>,
36    /// Number of function evaluations
37    pub n_evaluations: usize,
38    /// Number of generations/iterations
39    pub n_generations: usize,
40    /// Success flag
41    pub success: bool,
42    /// Convergence message
43    pub message: String,
44    /// Hypervolume indicator (if reference point provided)
45    pub hypervolume: Option<f64>,
46}
47
48/// Configuration for multi-objective optimization algorithms
49#[derive(Debug, Clone)]
50pub struct MultiObjectiveConfig {
51    /// Population size
52    pub population_size: usize,
53    /// Maximum number of generations
54    pub max_generations: usize,
55    /// Maximum number of function evaluations
56    pub max_evaluations: Option<usize>,
57    /// Crossover probability
58    pub crossover_probability: f64,
59    /// Mutation probability
60    pub mutation_probability: f64,
61    /// Mutation strength (eta for polynomial mutation)
62    pub mutation_eta: f64,
63    /// Crossover distribution index (eta for SBX)
64    pub crossover_eta: f64,
65    /// Convergence tolerance for hypervolume
66    pub tolerance: f64,
67    /// Reference point for hypervolume calculation
68    pub reference_point: Option<Array1<f64>>,
69    /// Variable bounds (lower, upper)
70    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
90/// NSGA-II (Non-dominated Sorting Genetic Algorithm II) implementation
91pub 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
100/// NSGA-III (Non-dominated Sorting Genetic Algorithm III) implementation
101/// Designed for many-objective optimization (4+ objectives)
102pub 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    /// Create new NSGA-II optimizer
114    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    /// Set variable bounds
132    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    /// Optimize multiple objectives
156    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        // Initialize population
166        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            // Check termination criteria
175            if let Some(max_evals) = self.config.max_evaluations {
176                if self.n_evaluations >= max_evals {
177                    break;
178                }
179            }
180
181            // Create offspring through selection, crossover, and mutation
182            let offspring = self.create_offspring(&mut objective_fn, constraint_fn.as_mut())?;
183
184            // Combine parent and offspring populations
185            let mut combined_population = self.population.clone();
186            combined_population.extend(offspring);
187
188            // Environmental selection (non-dominated sorting + crowding distance)
189            self.population = self.environmental_selection(combined_population);
190
191            // Check convergence
192            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        // Extract Pareto front (rank 0 solutions)
209        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    /// Initialize random population
234    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        // Perform initial ranking
277        self.population = self.environmental_selection(self.population.clone());
278
279        Ok(())
280    }
281
282    /// Create offspring through selection, crossover, and mutation
283    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            // Tournament selection
297            let parent1 = self.tournament_selection(&mut rng);
298            let parent2 = self.tournament_selection(&mut rng);
299
300            // Crossover
301            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            // Mutation
310            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            // Apply bounds
318            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            // Evaluate offspring
324            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    /// Tournament selection
355    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    /// Check if solution a dominates or is better than solution b
370    fn dominates_or_better(&self, a: &MultiObjectiveSolution, b: &MultiObjectiveSolution) -> bool {
371        // First check constraint dominance
372        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        // If both are feasible or both infeasible with same violation, check rank and crowding
379        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    /// Simulated Binary Crossover (SBX)
387    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    /// Polynomial mutation
420    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; // Scale mutation
435                } else {
436                    individual[i] += delta * 0.1;
437                }
438            }
439        }
440    }
441
442    /// Apply variable bounds
443    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    /// Environmental selection using non-dominated sorting and crowding distance
450    fn environmental_selection(
451        &self,
452        mut population: Vec<MultiObjectiveSolution>,
453    ) -> Vec<MultiObjectiveSolution> {
454        // Non-dominated sorting
455        let fronts = self.non_dominated_sorting(&mut population);
456
457        let mut new_population = Vec::new();
458        let mut front_idx = 0;
459
460        // Add fronts until population limit is reached
461        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 there's a partial front to add
469        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            // Sort by crowding distance (descending)
474            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    /// Non-dominated sorting
488    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        // Calculate domination relationships
499        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    /// Compare two solutions for dominance
545    fn compare_dominance(
546        &self,
547        a: &MultiObjectiveSolution,
548        b: &MultiObjectiveSolution,
549    ) -> Ordering {
550        // Handle constraint dominance
551        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        // Both solutions have same constraint status, check objective dominance
558        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    /// Calculate crowding distance for a front
579    fn calculate_crowding_distance(&self, front: &mut [MultiObjectiveSolution]) {
580        let n = front.len();
581
582        // Initialize crowding distances
583        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        // Calculate crowding distance for each objective
595        for obj in 0..self.n_objectives {
596            // Sort by objective value
597            front.sort_by(|a, b| {
598                a.objectives[obj]
599                    .partial_cmp(&b.objectives[obj])
600                    .unwrap_or(Ordering::Equal)
601            });
602
603            // Set boundary points to infinity
604            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    /// Calculate hypervolume indicator
619    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        // For 2D problems, use simple geometric calculation
637        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            // Sort by first objective in descending order for hypervolume calculation
644            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            // For higher dimensions, use Monte Carlo approximation
659            self.hypervolume_monte_carlo(reference_point, &pareto_front)
660        }
661    }
662
663    /// Monte Carlo hypervolume calculation for higher dimensions
664    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        // Find bounds for sampling
674        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            // Generate random point
683            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            // Check if point is dominated by any solution in Pareto front
690            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        // Calculate volume
711        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    /// Create new NSGA-III optimizer
722    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); // Default 12 divisions
729
730        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    /// Create NSGA-III with custom reference points
742    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    /// Set variable bounds
768    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    /// Generate structured reference points using Das and Dennis method
792    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        // Normalize points (they should sum to 1)
808        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    /// Recursive helper for generating reference points
821    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(&current_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    /// Calculate binomial coefficient
854    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); // Take advantage of symmetry
863        let mut result = 1;
864        for i in 0..k {
865            result = result * (n - i) / (i + 1);
866        }
867        result
868    }
869
870    /// Optimize multiple objectives using NSGA-III
871    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        // Initialize population
881        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            // Check termination criteria
887            if let Some(max_evals) = self.config.max_evaluations {
888                if self.n_evaluations >= max_evals {
889                    break;
890                }
891            }
892
893            // Create offspring
894            let offspring = self.create_offspring(&mut objective_fn, constraint_fn.as_mut())?;
895
896            // Combine parent and offspring populations
897            let mut combined_population = self.population.clone();
898            combined_population.extend(offspring);
899
900            // Environmental selection using reference point association
901            self.population = self.environmental_selection_nsga3(combined_population);
902        }
903
904        // Extract Pareto front (rank 0 solutions)
905        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    /// Initialize random population (similar to NSGA-II but reused here)
930    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    /// Create offspring (reuses NSGA-II logic)
976    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            // Tournament selection
990            let parent1 = self.tournament_selection(&mut rng);
991            let parent2 = self.tournament_selection(&mut rng);
992
993            // Crossover
994            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            // Mutation
1003            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            // Apply bounds
1011            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            // Evaluate offspring
1017            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    /// Environmental selection specific to NSGA-III using reference points
1048    fn environmental_selection_nsga3(
1049        &self,
1050        mut population: Vec<MultiObjectiveSolution>,
1051    ) -> Vec<MultiObjectiveSolution> {
1052        // Step 1: Non-dominated sorting
1053        let fronts = self.non_dominated_sorting(&mut population);
1054
1055        let mut new_population = Vec::new();
1056        let mut front_idx = 0;
1057
1058        // Step 2: Add complete fronts until we reach capacity
1059        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        // Step 3: If there's a partial front to add, use reference point association
1067        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            // Normalize objectives for the combined population
1072            let all_solutions: Vec<_> = new_population.iter().chain(partial_front.iter()).collect();
1073            let normalized_objectives = self.normalize_objectives(&all_solutions);
1074
1075            // Associate solutions with reference points
1076            let associations = self.associate_with_reference_points(
1077                &normalized_objectives,
1078                &new_population,
1079                partial_front,
1080            );
1081
1082            // Select solutions based on reference point niching
1083            let selected = self.reference_point_selection(partial_front, associations, remaining);
1084            new_population.extend(selected);
1085        }
1086
1087        new_population
1088    }
1089
1090    /// Normalize objectives using ideal and nadir points
1091    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        // Find ideal point (minimum in each objective)
1096        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        // Normalize objectives
1107        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    /// Associate solutions with reference points
1122    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        // Count associations for already selected solutions
1132        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        // Associate partial front solutions with reference points
1139        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    /// Find closest reference point for a solution
1150    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    /// Calculate perpendicular distance from point to reference direction
1172    fn perpendicular_distance(
1173        &self,
1174        point: &ArrayView1<f64>,
1175        reference_point: &ArrayView1<f64>,
1176    ) -> f64 {
1177        // Calculate projection length
1178        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        // Calculate perpendicular distance
1192        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    /// Select solutions using reference point niching
1202    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        // Calculate current niche counts (should be 0 for partial front)
1212
1213        for _ in 0..k {
1214            // Find reference point with minimum niche count that has associated solutions
1215            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            // Randomly select one reference point
1235            let mut rng = rng();
1236            let ref_point_idx = selected_ref_points[rng.random_range(0..selected_ref_points.len())];
1237
1238            // Select solution from this reference point
1239            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                    // If niche is empty, select closest solution
1243                    associated_indices[0] // Simplified: could compute actual closest
1244                } else {
1245                    // Random selection
1246                    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                // Remove selected solution from future consideration
1253                // (This is simplified - in practice would modify associations)
1254            }
1255        }
1256
1257        selected
1258    }
1259
1260    /// Non-dominated sorting (reuse from NSGA-II)
1261    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        // Calculate domination relationships
1272        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    /// Compare two solutions for dominance (reuse from NSGA-II logic)
1318    fn compare_dominance(
1319        &self,
1320        a: &MultiObjectiveSolution,
1321        b: &MultiObjectiveSolution,
1322    ) -> Ordering {
1323        // Handle constraint dominance
1324        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        // Both solutions have same constraint status, check objective dominance
1331        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    /// Tournament selection (reuse logic from NSGA-II)
1352    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    /// Check if solution a dominates or is better than solution b
1367    fn dominates_or_better(&self, a: &MultiObjectiveSolution, b: &MultiObjectiveSolution) -> bool {
1368        // First check constraint dominance
1369        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        // If both are feasible or both infeasible with same violation, check rank and crowding
1376        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    /// Simulated Binary Crossover (reuse from NSGA-II)
1384    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    /// Polynomial mutation (reuse from NSGA-II)
1417    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    /// Apply variable bounds (reuse from NSGA-II)
1440    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    /// Calculate hypervolume (reuse basic version, could be enhanced for many objectives)
1447    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        // For many objectives, use Monte Carlo approximation
1465        self.hypervolume_monte_carlo(reference_point, &pareto_front)
1466    }
1467
1468    /// Monte Carlo hypervolume calculation
1469    fn hypervolume_monte_carlo(
1470        &self,
1471        reference_point: &Array1<f64>,
1472        pareto_front: &[&MultiObjectiveSolution],
1473    ) -> Result<f64, OptimizeError> {
1474        let n_samples = 100000; // More samples for many objectives
1475        let mut rng = rng();
1476        let mut dominated_count = 0;
1477
1478        // Find bounds for sampling
1479        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            // Generate random point
1488            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            // Check if point is dominated by any solution in Pareto front
1495            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        // Calculate volume
1516        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
1525/// Scalarization methods for converting multi-objective to single-objective
1526pub mod scalarization {
1527    use super::*;
1528
1529    /// Weighted sum scalarization
1530    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    /// Weighted Tchebycheff scalarization
1543    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    /// Achievement Scalarizing Function (ASF)
1562    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; // Small augmentation parameter
1573
1574        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    /// ε-constraint method: minimize one objective subject to constraints on others
1591    pub struct EpsilonConstraint {
1592        /// Index of objective to minimize
1593        pub primary_objective: usize,
1594        /// Constraint bounds for other objectives
1595        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        /// Convert to single-objective problem with penalty for constraint violations
1607        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                // Add penalty for constraint violations
1623                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        // Simple bi-objective problem: minimize (x^2, (x-1)^2)
1645        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        // Check that Pareto front solutions are reasonable
1666        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        // sol3 dominates sol1 but not sol2
1705        assert_eq!(optimizer.compare_dominance(&sol3, &sol1), Ordering::Less);
1706        assert_eq!(optimizer.compare_dominance(&sol3, &sol2), Ordering::Equal);
1707
1708        // sol1 and sol2 are non-dominated with respect to each other
1709        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        // Test weighted sum
1720        let weighted_result = scalarization::weighted_sum(objectives_fn, &weights, &x.view());
1721        assert_abs_diff_eq!(weighted_result, 2.5, epsilon = 1e-10); // 0.5*1 + 0.5*4
1722
1723        // Test weighted Tchebycheff
1724        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); // max(0.5*1, 0.5*4)
1727    }
1728
1729    #[test]
1730    fn test_hypervolume_2d() {
1731        let config = MultiObjectiveConfig::default();
1732        let optimizer = NSGAII::new(1, 2, Some(config));
1733
1734        // Create a proper Pareto front (non-dominated points with decreasing y when x increases)
1735        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); // Should be less than total area
1774    }
1775}