1pub 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#[derive(Debug, Clone)]
25pub struct MultiObjectiveConfig {
26 pub population_size: usize,
28 pub max_generations: usize,
30 pub max_evaluations: Option<usize>,
32 pub crossover_probability: f64,
34 pub mutation_probability: f64,
36 pub mutation_eta: f64,
38 pub crossover_eta: f64,
40 pub tolerance: f64,
42 pub reference_point: Option<Array1<f64>>,
44 pub bounds: Option<(Array1<f64>, Array1<f64>)>,
46 pub random_seed: Option<u64>,
48 pub archive_size: Option<usize>,
50 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
74pub trait MultiObjectiveOptimizer {
76 fn optimize<F>(&mut self, objective_function: F) -> Result<MultiObjectiveResult, OptimizeError>
78 where
79 F: Fn(&ArrayView1<f64>) -> Array1<f64> + Send + Sync;
80
81 fn initialize_population(&mut self) -> Result<(), OptimizeError>;
83
84 fn evolve_generation<F>(&mut self, objective_function: &F) -> Result<(), OptimizeError>
86 where
87 F: Fn(&ArrayView1<f64>) -> Array1<f64> + Send + Sync;
88
89 fn check_convergence(&self) -> bool;
91
92 fn get_population(&self) -> &Population;
94
95 fn get_generation(&self) -> usize;
97
98 fn get_evaluations(&self) -> usize;
100
101 fn name(&self) -> &str;
103}
104
105pub 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
130pub struct OptimizerFactory;
132
133impl OptimizerFactory {
134 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 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 Ok(NSGAIII::new(
152 config.population_size,
153 n_objectives,
154 n_variables,
155 ))
156 }
157
158 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 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 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
213pub mod utils {
215 use super::*;
216 use scirs2_core::ndarray::Array2;
217
218 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 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 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 if reference_point.len() == 2 {
285 return calculate_hypervolume_2d(pareto_front, reference_point);
286 }
287
288 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 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 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 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 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 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 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 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 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 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 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 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); assert_eq!(individual[1], 1.0); }
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}