Skip to main content

scirs2_optimize/multi_objective/algorithms/
mod.rs

1//! Multi-objective optimization algorithms
2//!
3//! This module provides implementations of various multi-objective optimization algorithms:
4//! - NSGA-II (Non-dominated Sorting Genetic Algorithm II)
5//! - NSGA-III (Non-dominated Sorting Genetic Algorithm III)
6//! - MOEA/D (Multi-Objective Evolutionary Algorithm based on Decomposition)
7//! - SPEA2 (Strength Pareto Evolutionary Algorithm 2)
8
9pub mod moead;
10pub mod nsga2;
11pub mod nsga3;
12pub mod spea2;
13
14pub use moead::MOEAD;
15pub use nsga2::NSGAII;
16pub use nsga3::NSGAIII;
17pub use spea2::SPEA2;
18
19use super::solutions::{MultiObjectiveResult, MultiObjectiveSolution, Population};
20use crate::error::OptimizeError;
21use scirs2_core::ndarray::{Array1, ArrayView1};
22
23/// Configuration for multi-objective optimization algorithms
24#[derive(Debug, Clone)]
25pub struct MultiObjectiveConfig {
26    /// Population size
27    pub population_size: usize,
28    /// Maximum number of generations
29    pub max_generations: usize,
30    /// Maximum number of function evaluations
31    pub max_evaluations: Option<usize>,
32    /// Crossover probability
33    pub crossover_probability: f64,
34    /// Mutation probability
35    pub mutation_probability: f64,
36    /// Mutation strength (eta for polynomial mutation)
37    pub mutation_eta: f64,
38    /// Crossover distribution index (eta for SBX)
39    pub crossover_eta: f64,
40    /// Convergence tolerance for hypervolume
41    pub tolerance: f64,
42    /// Reference point for hypervolume calculation
43    pub reference_point: Option<Array1<f64>>,
44    /// Variable bounds (lower, upper)
45    pub bounds: Option<(Array1<f64>, Array1<f64>)>,
46    /// Random seed for reproducibility
47    pub random_seed: Option<u64>,
48    /// Archive size (for algorithms that use archives)
49    pub archive_size: Option<usize>,
50    /// Neighborhood size (for MOEA/D)
51    pub neighborhood_size: Option<usize>,
52}
53
54impl Default for MultiObjectiveConfig {
55    fn default() -> Self {
56        Self {
57            population_size: 100,
58            max_generations: 250,
59            max_evaluations: None,
60            crossover_probability: 0.9,
61            mutation_probability: 0.1,
62            mutation_eta: 20.0,
63            crossover_eta: 15.0,
64            tolerance: 1e-6,
65            reference_point: None,
66            bounds: None,
67            random_seed: None,
68            archive_size: None,
69            neighborhood_size: None,
70        }
71    }
72}
73
74/// Trait for multi-objective optimization algorithms
75pub trait MultiObjectiveOptimizer {
76    /// Optimize the problem
77    fn optimize<F>(&mut self, objective_function: F) -> Result<MultiObjectiveResult, OptimizeError>
78    where
79        F: Fn(&ArrayView1<f64>) -> Array1<f64> + Send + Sync;
80
81    /// Initialize the population
82    fn initialize_population(&mut self) -> Result<(), OptimizeError>;
83
84    /// Perform one generation/iteration
85    fn evolve_generation<F>(&mut self, objective_function: &F) -> Result<(), OptimizeError>
86    where
87        F: Fn(&ArrayView1<f64>) -> Array1<f64> + Send + Sync;
88
89    /// Check convergence criteria
90    fn check_convergence(&self) -> bool;
91
92    /// Get current population
93    fn get_population(&self) -> &Population;
94
95    /// Get current generation number
96    fn get_generation(&self) -> usize;
97
98    /// Get number of function evaluations
99    fn get_evaluations(&self) -> usize;
100
101    /// Get algorithm name
102    fn name(&self) -> &str;
103}
104
105/// Wrapper enum for different multi-objective optimizers
106pub enum MultiObjectiveOptimizerWrapper {
107    NSGAII(NSGAII),
108    NSGAIII(NSGAIII),
109    MOEAD(MOEAD),
110    SPEA2(SPEA2),
111}
112
113impl MultiObjectiveOptimizerWrapper {
114    pub fn optimize<F>(
115        &mut self,
116        objective_function: F,
117    ) -> Result<MultiObjectiveResult, OptimizeError>
118    where
119        F: Fn(&ArrayView1<f64>) -> Array1<f64> + Send + Sync,
120    {
121        match self {
122            Self::NSGAII(opt) => opt.optimize(objective_function),
123            Self::NSGAIII(opt) => opt.optimize(objective_function),
124            Self::MOEAD(opt) => opt.optimize(objective_function),
125            Self::SPEA2(opt) => opt.optimize(objective_function),
126        }
127    }
128}
129
130/// Factory for creating multi-objective optimizers
131pub struct OptimizerFactory;
132
133impl OptimizerFactory {
134    /// Create NSGA-II optimizer
135    pub fn create_nsga2(
136        config: MultiObjectiveConfig,
137        n_objectives: usize,
138        n_variables: usize,
139    ) -> Result<NSGAII, OptimizeError> {
140        NSGAII::new(config, n_objectives, n_variables)
141    }
142
143    /// Create NSGA-III optimizer
144    pub fn create_nsga3(
145        config: MultiObjectiveConfig,
146        n_objectives: usize,
147        n_variables: usize,
148        reference_points: Option<Vec<Array1<f64>>>,
149    ) -> Result<NSGAIII, OptimizeError> {
150        // TODO: Use reference_points when NSGA-III is fully implemented
151        Ok(NSGAIII::new(
152            config.population_size,
153            n_objectives,
154            n_variables,
155        ))
156    }
157
158    /// Create MOEA/D optimizer
159    pub fn create_moead(
160        config: MultiObjectiveConfig,
161        n_objectives: usize,
162        n_variables: usize,
163    ) -> MOEAD {
164        MOEAD::with_config(
165            config,
166            n_objectives,
167            n_variables,
168            moead::ScalarizationMethod::Tchebycheff,
169        )
170    }
171
172    /// Create SPEA2 optimizer
173    pub fn create_spea2(
174        config: MultiObjectiveConfig,
175        n_objectives: usize,
176        n_variables: usize,
177    ) -> SPEA2 {
178        SPEA2::with_config(config, n_objectives, n_variables)
179    }
180
181    /// Create optimizer by name
182    pub fn create_by_name(
183        algorithm: &str,
184        config: MultiObjectiveConfig,
185        n_objectives: usize,
186        n_variables: usize,
187    ) -> Result<MultiObjectiveOptimizerWrapper, OptimizeError> {
188        match algorithm.to_lowercase().as_str() {
189            "nsga2" | "nsga-ii" => Ok(MultiObjectiveOptimizerWrapper::NSGAII(Self::create_nsga2(
190                config,
191                n_objectives,
192                n_variables,
193            )?)),
194            "nsga3" | "nsga-iii" => Ok(MultiObjectiveOptimizerWrapper::NSGAIII(
195                Self::create_nsga3(config, n_objectives, n_variables, None)?,
196            )),
197            "moead" | "moea/d" | "moea-d" => Ok(MultiObjectiveOptimizerWrapper::MOEAD(
198                Self::create_moead(config, n_objectives, n_variables),
199            )),
200            "spea2" => Ok(MultiObjectiveOptimizerWrapper::SPEA2(Self::create_spea2(
201                config,
202                n_objectives,
203                n_variables,
204            ))),
205            _ => Err(OptimizeError::InvalidInput(format!(
206                "Unknown algorithm: {}",
207                algorithm
208            ))),
209        }
210    }
211}
212
213/// Utility functions for multi-objective optimization
214pub mod utils {
215    use super::*;
216    use scirs2_core::ndarray::Array2;
217
218    /// Generate Das-Dennis reference points for NSGA-III
219    pub fn generate_das_dennis_points(
220        n_objectives: usize,
221        n_partitions: usize,
222    ) -> Vec<Array1<f64>> {
223        if n_objectives == 1 {
224            return vec![Array1::from_vec(vec![1.0])];
225        }
226
227        let mut points = Vec::new();
228        generate_das_dennis_recursive(
229            &mut points,
230            Array1::zeros(n_objectives),
231            0,
232            n_objectives,
233            n_partitions,
234            n_partitions,
235        );
236
237        // Normalize points
238        for point in &mut points {
239            let sum: f64 = point.sum();
240            if sum > 0.0 {
241                *point /= sum;
242            }
243        }
244
245        points
246    }
247
248    fn generate_das_dennis_recursive(
249        points: &mut Vec<Array1<f64>>,
250        mut current_point: Array1<f64>,
251        index: usize,
252        n_objectives: usize,
253        n_partitions: usize,
254        remaining: usize,
255    ) {
256        if index == n_objectives - 1 {
257            current_point[index] = remaining as f64;
258            points.push(current_point);
259        } else {
260            for i in 0..=remaining {
261                current_point[index] = i as f64;
262                generate_das_dennis_recursive(
263                    points,
264                    current_point.clone(),
265                    index + 1,
266                    n_objectives,
267                    n_partitions,
268                    remaining - i,
269                );
270            }
271        }
272    }
273
274    /// Calculate hypervolume indicator
275    pub fn calculate_hypervolume(
276        pareto_front: &[MultiObjectiveSolution],
277        reference_point: &Array1<f64>,
278    ) -> f64 {
279        if pareto_front.is_empty() {
280            return 0.0;
281        }
282
283        // For 2D case, use simple calculation
284        if reference_point.len() == 2 {
285            return calculate_hypervolume_2d(pareto_front, reference_point);
286        }
287
288        // For higher dimensions, use Monte Carlo approximation
289        calculate_hypervolume_monte_carlo(pareto_front, reference_point, 10000)
290    }
291
292    fn calculate_hypervolume_2d(
293        pareto_front: &[MultiObjectiveSolution],
294        reference_point: &Array1<f64>,
295    ) -> f64 {
296        let mut points: Vec<_> = pareto_front
297            .iter()
298            .map(|sol| (sol.objectives[0], sol.objectives[1]))
299            .collect();
300
301        // Sort by first objective in descending order for correct hypervolume calculation
302        points.sort_by(|a, b| b.0.total_cmp(&a.0));
303
304        let mut hypervolume = 0.0;
305        let mut prev_x = reference_point[0];
306
307        for (x, y) in points {
308            if x < reference_point[0] && y < reference_point[1] {
309                hypervolume += (prev_x - x) * (reference_point[1] - y);
310                prev_x = x;
311            }
312        }
313
314        hypervolume
315    }
316
317    fn calculate_hypervolume_monte_carlo(
318        pareto_front: &[MultiObjectiveSolution],
319        reference_point: &Array1<f64>,
320        n_samples: usize,
321    ) -> f64 {
322        use scirs2_core::random::prelude::*;
323        let mut rng = scirs2_core::random::rng();
324        let n_objectives = reference_point.len();
325
326        // Find bounds for sampling
327        let mut min_bounds = Array1::from_elem(n_objectives, f64::INFINITY);
328        for sol in pareto_front {
329            for (i, &obj) in sol.objectives.iter().enumerate() {
330                min_bounds[i] = min_bounds[i].min(obj);
331            }
332        }
333
334        let mut dominated_count = 0;
335        for _ in 0..n_samples {
336            // Generate random point
337            let mut point = Array1::zeros(n_objectives);
338            for i in 0..n_objectives {
339                point[i] = rng.random_range(min_bounds[i]..reference_point[i]);
340            }
341
342            // Check if point is dominated by any solution in Pareto front
343            for sol in pareto_front {
344                let mut dominates = true;
345                for (i, &obj) in sol.objectives.iter().enumerate() {
346                    if obj >= point[i] {
347                        dominates = false;
348                        break;
349                    }
350                }
351                if dominates {
352                    dominated_count += 1;
353                    break;
354                }
355            }
356        }
357
358        // Calculate volume
359        let total_volume: f64 = (0..n_objectives)
360            .map(|i| reference_point[i] - min_bounds[i])
361            .product();
362
363        total_volume * (dominated_count as f64 / n_samples as f64)
364    }
365
366    /// Calculate diversity metric (spacing)
367    pub fn calculate_spacing(pareto_front: &[MultiObjectiveSolution]) -> f64 {
368        if pareto_front.len() < 2 {
369            return 0.0;
370        }
371
372        let distances: Vec<f64> = pareto_front
373            .iter()
374            .map(|sol1| {
375                pareto_front
376                    .iter()
377                    .filter(|sol2| !std::ptr::eq(sol1, *sol2))
378                    .map(|sol2| sol1.objective_distance(sol2))
379                    .fold(f64::INFINITY, f64::min)
380            })
381            .collect();
382
383        let mean_distance = distances.iter().sum::<f64>() / distances.len() as f64;
384        let variance = distances
385            .iter()
386            .map(|d| (d - mean_distance).powi(2))
387            .sum::<f64>()
388            / distances.len() as f64;
389
390        variance.sqrt()
391    }
392
393    /// Calculate convergence metric (average distance to true Pareto front)
394    pub fn calculate_convergence(
395        pareto_front: &[MultiObjectiveSolution],
396        true_pareto_front: &[MultiObjectiveSolution],
397    ) -> f64 {
398        if pareto_front.is_empty() || true_pareto_front.is_empty() {
399            return f64::INFINITY;
400        }
401
402        let total_distance: f64 = pareto_front
403            .iter()
404            .map(|sol1| {
405                true_pareto_front
406                    .iter()
407                    .map(|sol2| sol1.objective_distance(sol2))
408                    .fold(f64::INFINITY, f64::min)
409            })
410            .sum();
411
412        total_distance / pareto_front.len() as f64
413    }
414
415    /// Generate random initial population
416    pub fn generate_random_population(
417        size: usize,
418        n_variables: usize,
419        bounds: &Option<(Array1<f64>, Array1<f64>)>,
420    ) -> Vec<Array1<f64>> {
421        use scirs2_core::random::prelude::*;
422        let mut rng = scirs2_core::random::rng();
423        let mut population = Vec::with_capacity(size);
424
425        let (lower, upper) = match bounds {
426            Some((l, u)) => (l.clone(), u.clone()),
427            None => (Array1::zeros(n_variables), Array1::ones(n_variables)),
428        };
429
430        for _ in 0..size {
431            let mut individual = Array1::zeros(n_variables);
432            for j in 0..n_variables {
433                individual[j] = rng.random_range(lower[j]..upper[j]);
434            }
435            population.push(individual);
436        }
437
438        population
439    }
440
441    /// Apply bounds to a solution
442    pub fn apply_bounds(individual: &mut Array1<f64>, bounds: &Option<(Array1<f64>, Array1<f64>)>) {
443        if let Some((lower, upper)) = bounds {
444            for (i, value) in individual.iter_mut().enumerate() {
445                *value = value.max(lower[i]).min(upper[i]);
446            }
447        }
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454    use scirs2_core::ndarray::array;
455
456    #[test]
457    fn test_config_default() {
458        let config = MultiObjectiveConfig::default();
459        assert_eq!(config.population_size, 100);
460        assert_eq!(config.max_generations, 250);
461        assert_eq!(config.crossover_probability, 0.9);
462    }
463
464    #[test]
465    fn test_das_dennis_points_2d() {
466        let points = utils::generate_das_dennis_points(2, 3);
467        assert!(!points.is_empty());
468
469        // Check that points are normalized
470        for point in &points {
471            let sum: f64 = point.sum();
472            assert!((sum - 1.0).abs() < 1e-10);
473        }
474    }
475
476    #[test]
477    fn test_hypervolume_2d() {
478        let solutions = vec![
479            MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]),
480            MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]),
481            MultiObjectiveSolution::new(array![3.0], array![3.0, 1.0]),
482        ];
483
484        let reference_point = array![4.0, 4.0];
485        let hv = utils::calculate_hypervolume(&solutions, &reference_point);
486        assert!(hv > 0.0);
487    }
488
489    #[test]
490    fn test_spacing_calculation() {
491        let solutions = vec![
492            MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]),
493            MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]),
494            MultiObjectiveSolution::new(array![3.0], array![3.0, 1.0]),
495        ];
496
497        let spacing = utils::calculate_spacing(&solutions);
498        assert!(spacing >= 0.0);
499    }
500
501    #[test]
502    fn test_random_population_generation() {
503        let bounds = Some((array![0.0, -1.0], array![1.0, 1.0]));
504        let population = utils::generate_random_population(10, 2, &bounds);
505
506        assert_eq!(population.len(), 10);
507        assert_eq!(population[0].len(), 2);
508
509        // Check bounds
510        for individual in &population {
511            assert!(individual[0] >= 0.0 && individual[0] <= 1.0);
512            assert!(individual[1] >= -1.0 && individual[1] <= 1.0);
513        }
514    }
515
516    #[test]
517    fn test_apply_bounds() {
518        let bounds = Some((array![0.0, -1.0], array![1.0, 1.0]));
519        let mut individual = array![-0.5, 2.0];
520
521        utils::apply_bounds(&mut individual, &bounds);
522
523        assert_eq!(individual[0], 0.0); // Clamped to lower bound
524        assert_eq!(individual[1], 1.0); // Clamped to upper bound
525    }
526
527    #[test]
528    fn test_optimizer_factory() {
529        let config = MultiObjectiveConfig::default();
530
531        let nsga2 = OptimizerFactory::create_by_name("nsga2", config.clone(), 2, 3);
532        assert!(nsga2.is_ok());
533
534        let nsga3 = OptimizerFactory::create_by_name("nsga3", config.clone(), 2, 3);
535        assert!(nsga3.is_ok());
536
537        let unknown = OptimizerFactory::create_by_name("unknown", config, 2, 3);
538        assert!(unknown.is_err());
539    }
540}