1use super::{utils, MultiObjectiveConfig, MultiObjectiveOptimizer};
20use crate::error::OptimizeError;
21use crate::multi_objective::solutions::{MultiObjectiveResult, MultiObjectiveSolution, Population};
22use scirs2_core::ndarray::{Array1, ArrayView1};
23use scirs2_core::random::rngs::StdRng;
24use scirs2_core::random::{Rng, RngExt, SeedableRng};
25
26#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum ScalarizationMethod {
29 Tchebycheff,
31 WeightedSum,
33 PBI,
35}
36
37impl Default for ScalarizationMethod {
38 fn default() -> Self {
39 ScalarizationMethod::Tchebycheff
40 }
41}
42
43pub struct MOEAD {
45 config: MultiObjectiveConfig,
46 n_objectives: usize,
47 n_variables: usize,
48 weight_vectors: Vec<Array1<f64>>,
50 neighborhoods: Vec<Vec<usize>>,
52 neighborhood_size: usize,
54 solutions: Vec<MultiObjectiveSolution>,
56 ideal_point: Array1<f64>,
58 scalarization: ScalarizationMethod,
60 population: Population,
61 generation: usize,
62 n_evaluations: usize,
63 rng: StdRng,
64 cr: f64,
66 f_scale: f64,
68 delta: f64,
70 nr: usize,
72 convergence_history: Vec<f64>,
73}
74
75impl MOEAD {
76 pub fn new(population_size: usize, n_objectives: usize, n_variables: usize) -> Self {
78 let config = MultiObjectiveConfig {
79 population_size,
80 ..Default::default()
81 };
82 Self::with_config(
83 config,
84 n_objectives,
85 n_variables,
86 ScalarizationMethod::Tchebycheff,
87 )
88 }
89
90 pub fn with_config(
92 config: MultiObjectiveConfig,
93 n_objectives: usize,
94 n_variables: usize,
95 scalarization: ScalarizationMethod,
96 ) -> Self {
97 let neighborhood_size = config.neighborhood_size.unwrap_or(20);
98 let neighborhood_size = neighborhood_size.min(config.population_size);
99
100 let seed = config.random_seed.unwrap_or_else(|| {
101 use std::time::{SystemTime, UNIX_EPOCH};
102 SystemTime::now()
103 .duration_since(UNIX_EPOCH)
104 .map(|d| d.as_secs())
105 .unwrap_or(42)
106 });
107
108 let rng = StdRng::seed_from_u64(seed);
109
110 let weight_vectors = generate_uniform_weight_vectors(n_objectives, config.population_size);
112
113 let actual_pop_size = weight_vectors.len();
114
115 let neighborhoods = compute_neighborhoods(&weight_vectors, neighborhood_size);
117
118 Self {
119 config: MultiObjectiveConfig {
120 population_size: actual_pop_size,
121 ..config
122 },
123 n_objectives,
124 n_variables,
125 weight_vectors,
126 neighborhoods,
127 neighborhood_size,
128 solutions: Vec::new(),
129 ideal_point: Array1::from_elem(n_objectives, f64::INFINITY),
130 scalarization,
131 population: Population::new(),
132 generation: 0,
133 n_evaluations: 0,
134 rng,
135 cr: 1.0, f_scale: 0.5, delta: 0.9, nr: 2, convergence_history: Vec::new(),
140 }
141 }
142
143 fn evaluate_individual<F>(
145 &mut self,
146 variables: &Array1<f64>,
147 objective_function: &F,
148 ) -> Result<Array1<f64>, OptimizeError>
149 where
150 F: Fn(&ArrayView1<f64>) -> Array1<f64> + Send + Sync,
151 {
152 self.n_evaluations += 1;
153
154 if let Some(max_evals) = self.config.max_evaluations {
155 if self.n_evaluations > max_evals {
156 return Err(OptimizeError::MaxEvaluationsReached);
157 }
158 }
159
160 let objectives = objective_function(&variables.view());
161 if objectives.len() != self.n_objectives {
162 return Err(OptimizeError::InvalidInput(format!(
163 "Expected {} objectives, got {}",
164 self.n_objectives,
165 objectives.len()
166 )));
167 }
168
169 Ok(objectives)
170 }
171
172 fn update_ideal_point(&mut self, objectives: &Array1<f64>) {
174 for (i, &obj) in objectives.iter().enumerate() {
175 if obj < self.ideal_point[i] {
176 self.ideal_point[i] = obj;
177 }
178 }
179 }
180
181 fn scalar_value(&self, objectives: &Array1<f64>, weight: &Array1<f64>) -> f64 {
183 match self.scalarization {
184 ScalarizationMethod::Tchebycheff => {
185 objectives
187 .iter()
188 .zip(weight.iter())
189 .zip(self.ideal_point.iter())
190 .map(|((obj, w), ideal)| {
191 let w_eff = if *w < 1e-10 { 1e-10 } else { *w };
192 w_eff * (obj - ideal).abs()
193 })
194 .fold(0.0_f64, f64::max)
195 }
196 ScalarizationMethod::WeightedSum => {
197 objectives
199 .iter()
200 .zip(weight.iter())
201 .map(|(obj, w)| w * obj)
202 .sum()
203 }
204 ScalarizationMethod::PBI => {
205 let theta = 5.0; let diff: Array1<f64> = objectives - &self.ideal_point;
211 let w_norm = weight.dot(weight).sqrt();
212 if w_norm < 1e-30 {
213 return diff.dot(&diff).sqrt();
214 }
215
216 let w_unit = weight / w_norm;
217 let d1 = diff.dot(&w_unit).abs();
218 let proj = d1 * &w_unit;
219 let perp = &diff - &proj;
220 let d2 = perp.dot(&perp).sqrt();
221
222 d1 + theta * d2
223 }
224 }
225 }
226
227 fn generate_offspring(&mut self, subproblem_idx: usize) -> Array1<f64> {
229 let pool = if self.rng.random::<f64>() < self.delta {
230 &self.neighborhoods[subproblem_idx]
232 } else {
233 &self.neighborhoods[subproblem_idx]
235 };
236
237 if pool.len() < 3 || self.solutions.len() < 3 {
238 let current = &self.solutions[subproblem_idx].variables;
240 let mut child = current.clone();
241 let bounds: Vec<(f64, f64)> = if let Some((lower, upper)) = &self.config.bounds {
242 lower
243 .iter()
244 .zip(upper.iter())
245 .map(|(&l, &u)| (l, u))
246 .collect()
247 } else {
248 vec![(-1.0, 1.0); self.n_variables]
249 };
250
251 for j in 0..self.n_variables {
252 if self.rng.random::<f64>() < 0.1 {
253 let (lb, ub) = bounds[j];
254 child[j] = self.rng.random_range(lb..ub);
255 }
256 }
257 return child;
258 }
259
260 let mut indices = Vec::with_capacity(3);
262 let mut attempts = 0;
263 while indices.len() < 3 && attempts < 100 {
264 let idx = pool[self.rng.random_range(0..pool.len())];
265 if idx < self.solutions.len() && !indices.contains(&idx) && idx != subproblem_idx {
266 indices.push(idx);
267 }
268 attempts += 1;
269 }
270
271 if indices.len() < 3 {
273 let current = &self.solutions[subproblem_idx].variables;
274 return current.clone();
275 }
276
277 let x1 = &self.solutions[indices[0]].variables;
279 let x2 = &self.solutions[indices[1]].variables;
280 let x3 = &self.solutions[indices[2]].variables;
281
282 let current = &self.solutions[subproblem_idx].variables;
283 let mut child = Array1::zeros(self.n_variables);
284
285 let j_rand = self.rng.random_range(0..self.n_variables);
286 for j in 0..self.n_variables {
287 if self.rng.random::<f64>() < self.cr || j == j_rand {
288 child[j] = x1[j] + self.f_scale * (x2[j] - x3[j]);
289 } else {
290 child[j] = current[j];
291 }
292 }
293
294 if let Some((lower, upper)) = &self.config.bounds {
296 for j in 0..self.n_variables {
297 child[j] = child[j].max(lower[j]).min(upper[j]);
298 }
299 }
300
301 child
302 }
303
304 fn update_neighbors(&mut self, subproblem_idx: usize, offspring: &MultiObjectiveSolution) {
306 let neighbors = self.neighborhoods[subproblem_idx].clone();
307 let mut count = 0;
308
309 for &neighbor_idx in &neighbors {
310 if count >= self.nr {
311 break;
312 }
313 if neighbor_idx >= self.solutions.len() || neighbor_idx >= self.weight_vectors.len() {
314 continue;
315 }
316
317 let w = &self.weight_vectors[neighbor_idx];
318 let new_scalar = self.scalar_value(&offspring.objectives, w);
319 let old_scalar = self.scalar_value(&self.solutions[neighbor_idx].objectives, w);
320
321 if new_scalar < old_scalar {
322 self.solutions[neighbor_idx] = offspring.clone();
323 count += 1;
324 }
325 }
326 }
327
328 fn calculate_metrics(&mut self) {
330 if let Some(ref_point) = &self.config.reference_point {
331 let pop = Population::from_solutions(self.solutions.clone());
332 let pareto_front = pop.extract_pareto_front();
333 let hv = utils::calculate_hypervolume(&pareto_front, ref_point);
334 self.convergence_history.push(hv);
335 }
336 }
337}
338
339impl MultiObjectiveOptimizer for MOEAD {
340 fn optimize<F>(&mut self, objective_function: F) -> Result<MultiObjectiveResult, OptimizeError>
341 where
342 F: Fn(&ArrayView1<f64>) -> Array1<f64> + Send + Sync,
343 {
344 self.initialize_population()?;
345
346 let pop_size = self.weight_vectors.len();
348 let initial_vars =
349 utils::generate_random_population(pop_size, self.n_variables, &self.config.bounds);
350
351 self.solutions.clear();
352 for vars in initial_vars {
353 let objs = self.evaluate_individual(&vars, &objective_function)?;
354 self.update_ideal_point(&objs);
355 self.solutions.push(MultiObjectiveSolution::new(vars, objs));
356 }
357
358 while self.generation < self.config.max_generations {
360 if self.check_convergence() {
361 break;
362 }
363 self.evolve_generation(&objective_function)?;
364 }
365
366 let pop = Population::from_solutions(self.solutions.clone());
368 let pareto_front = pop.extract_pareto_front();
369 let hypervolume = self
370 .config
371 .reference_point
372 .as_ref()
373 .map(|rp| utils::calculate_hypervolume(&pareto_front, rp));
374
375 let mut result = MultiObjectiveResult::new(
376 pareto_front,
377 self.solutions.clone(),
378 self.n_evaluations,
379 self.generation,
380 );
381 result.hypervolume = hypervolume;
382 result.metrics.convergence_history = self.convergence_history.clone();
383
384 Ok(result)
385 }
386
387 fn evolve_generation<F>(&mut self, objective_function: &F) -> Result<(), OptimizeError>
388 where
389 F: Fn(&ArrayView1<f64>) -> Array1<f64> + Send + Sync,
390 {
391 let pop_size = self.solutions.len();
392
393 for i in 0..pop_size {
394 let child_vars = self.generate_offspring(i);
396
397 let child_objs = self.evaluate_individual(&child_vars, objective_function)?;
399
400 self.update_ideal_point(&child_objs);
402
403 let offspring = MultiObjectiveSolution::new(child_vars, child_objs);
404
405 self.update_neighbors(i, &offspring);
407 }
408
409 self.generation += 1;
410
411 self.population = Population::from_solutions(self.solutions.clone());
413
414 self.calculate_metrics();
415
416 Ok(())
417 }
418
419 fn initialize_population(&mut self) -> Result<(), OptimizeError> {
420 self.solutions.clear();
421 self.population.clear();
422 self.generation = 0;
423 self.n_evaluations = 0;
424 self.ideal_point = Array1::from_elem(self.n_objectives, f64::INFINITY);
425 self.convergence_history.clear();
426 Ok(())
427 }
428
429 fn check_convergence(&self) -> bool {
430 if let Some(max_evals) = self.config.max_evaluations {
431 if self.n_evaluations >= max_evals {
432 return true;
433 }
434 }
435
436 if self.convergence_history.len() >= 10 {
437 let recent = &self.convergence_history[self.convergence_history.len() - 10..];
438 let max_hv = recent.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
439 let min_hv = recent.iter().fold(f64::INFINITY, |a, &b| a.min(b));
440 if (max_hv - min_hv) < self.config.tolerance {
441 return true;
442 }
443 }
444
445 false
446 }
447
448 fn get_population(&self) -> &Population {
449 &self.population
450 }
451
452 fn get_generation(&self) -> usize {
453 self.generation
454 }
455
456 fn get_evaluations(&self) -> usize {
457 self.n_evaluations
458 }
459
460 fn name(&self) -> &str {
461 "MOEA/D"
462 }
463}
464
465fn generate_uniform_weight_vectors(n_objectives: usize, target_size: usize) -> Vec<Array1<f64>> {
468 if n_objectives == 0 {
469 return vec![];
470 }
471 if n_objectives == 1 {
472 return vec![Array1::from_elem(1, 1.0); target_size.max(1)];
473 }
474
475 let mut h = 1;
477 loop {
478 let n_points = binomial_coefficient(h + n_objectives - 1, n_objectives - 1);
479 if n_points >= target_size {
480 break;
481 }
482 h += 1;
483 if h > 100 {
484 break;
485 }
486 }
487
488 let mut vectors = Vec::new();
489 generate_weight_recursive(
490 &mut vectors,
491 Array1::zeros(n_objectives),
492 0,
493 n_objectives,
494 h,
495 h,
496 );
497
498 for v in &mut vectors {
500 let sum: f64 = v.sum();
501 if sum > 0.0 {
502 *v /= sum;
503 }
504 }
505
506 if vectors.len() > target_size {
508 vectors.truncate(target_size);
509 }
510
511 vectors
512}
513
514fn generate_weight_recursive(
516 vectors: &mut Vec<Array1<f64>>,
517 mut current: Array1<f64>,
518 index: usize,
519 n_objectives: usize,
520 h: usize,
521 remaining: usize,
522) {
523 if index == n_objectives - 1 {
524 current[index] = remaining as f64;
525 vectors.push(current);
526 } else {
527 for i in 0..=remaining {
528 current[index] = i as f64;
529 generate_weight_recursive(
530 vectors,
531 current.clone(),
532 index + 1,
533 n_objectives,
534 h,
535 remaining - i,
536 );
537 }
538 }
539}
540
541fn binomial_coefficient(n: usize, k: usize) -> usize {
543 if k > n {
544 return 0;
545 }
546 if k == 0 || k == n {
547 return 1;
548 }
549 let k = k.min(n - k);
550 let mut result = 1usize;
551 for i in 0..k {
552 result = result.saturating_mul(n - i);
553 result /= i + 1;
554 }
555 result
556}
557
558fn compute_neighborhoods(
560 weight_vectors: &[Array1<f64>],
561 neighborhood_size: usize,
562) -> Vec<Vec<usize>> {
563 let n = weight_vectors.len();
564 let t = neighborhood_size.min(n);
565
566 let mut neighborhoods = Vec::with_capacity(n);
567
568 for i in 0..n {
569 let mut distances: Vec<(usize, f64)> = (0..n)
571 .map(|j| {
572 let diff = &weight_vectors[i] - &weight_vectors[j];
573 let dist = diff.dot(&diff).sqrt();
574 (j, dist)
575 })
576 .collect();
577
578 distances.sort_by(|a, b| a.1.total_cmp(&b.1));
580
581 let neighbors: Vec<usize> = distances.iter().take(t).map(|(idx, _)| *idx).collect();
583 neighborhoods.push(neighbors);
584 }
585
586 neighborhoods
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592 use scirs2_core::ndarray::{array, s};
593
594 fn zdt1(x: &ArrayView1<f64>) -> Array1<f64> {
595 let f1 = x[0];
596 let g = 1.0 + 9.0 * x.slice(s![1..]).sum() / (x.len() - 1) as f64;
597 let f2 = g * (1.0 - (f1 / g).sqrt());
598 array![f1, f2]
599 }
600
601 #[test]
602 fn test_moead_creation() {
603 let moead = MOEAD::new(50, 2, 3);
604 assert_eq!(moead.n_objectives, 2);
605 assert_eq!(moead.n_variables, 3);
606 assert!(!moead.weight_vectors.is_empty());
607 assert_eq!(moead.generation, 0);
608 }
609
610 #[test]
611 fn test_moead_with_config() {
612 let config = MultiObjectiveConfig {
613 population_size: 30,
614 neighborhood_size: Some(10),
615 max_generations: 10,
616 random_seed: Some(42),
617 ..Default::default()
618 };
619
620 let moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::Tchebycheff);
621 assert_eq!(moead.neighborhood_size, 10);
622 }
623
624 #[test]
625 fn test_moead_optimize_zdt1() {
626 let config = MultiObjectiveConfig {
627 max_generations: 10,
628 population_size: 20,
629 bounds: Some((Array1::zeros(3), Array1::ones(3))),
630 random_seed: Some(42),
631 neighborhood_size: Some(5),
632 ..Default::default()
633 };
634
635 let mut moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::Tchebycheff);
636 let result = moead.optimize(zdt1);
637
638 assert!(result.is_ok());
639 let res = result.expect("should succeed");
640 assert!(res.success);
641 assert!(!res.pareto_front.is_empty());
642 assert!(res.n_evaluations > 0);
643 }
644
645 #[test]
646 fn test_moead_weighted_sum() {
647 let config = MultiObjectiveConfig {
648 max_generations: 10,
649 population_size: 20,
650 bounds: Some((Array1::zeros(3), Array1::ones(3))),
651 random_seed: Some(42),
652 ..Default::default()
653 };
654
655 let mut moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::WeightedSum);
656 let result = moead.optimize(zdt1);
657
658 assert!(result.is_ok());
659 let res = result.expect("should succeed");
660 assert!(res.success);
661 }
662
663 #[test]
664 fn test_moead_pbi() {
665 let config = MultiObjectiveConfig {
666 max_generations: 10,
667 population_size: 20,
668 bounds: Some((Array1::zeros(3), Array1::ones(3))),
669 random_seed: Some(42),
670 ..Default::default()
671 };
672
673 let mut moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::PBI);
674 let result = moead.optimize(zdt1);
675
676 assert!(result.is_ok());
677 let res = result.expect("should succeed");
678 assert!(res.success);
679 }
680
681 #[test]
682 fn test_moead_max_evaluations() {
683 let config = MultiObjectiveConfig {
684 max_generations: 1000,
685 max_evaluations: Some(50),
686 population_size: 10,
687 bounds: Some((Array1::zeros(3), Array1::ones(3))),
688 random_seed: Some(42),
689 ..Default::default()
690 };
691
692 let mut moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::Tchebycheff);
693 let result = moead.optimize(zdt1);
694 assert!(result.is_ok());
695 let res = result.expect("should succeed");
696 assert!(res.n_evaluations <= 70); }
698
699 #[test]
700 fn test_weight_vector_generation() {
701 let vectors = generate_uniform_weight_vectors(2, 10);
702 assert!(!vectors.is_empty());
703
704 for v in &vectors {
706 let sum: f64 = v.sum();
707 assert!(
708 (sum - 1.0).abs() < 1e-10,
709 "Weight sum should be 1.0, got {}",
710 sum
711 );
712 }
713
714 for v in &vectors {
716 for &w in v.iter() {
717 assert!(w >= -1e-10, "Weight should be >= 0, got {}", w);
718 }
719 }
720 }
721
722 #[test]
723 fn test_weight_vector_3d() {
724 let vectors = generate_uniform_weight_vectors(3, 20);
725 assert!(!vectors.is_empty());
726
727 for v in &vectors {
728 assert_eq!(v.len(), 3);
729 let sum: f64 = v.sum();
730 assert!((sum - 1.0).abs() < 1e-10);
731 }
732 }
733
734 #[test]
735 fn test_neighborhood_computation() {
736 let vectors = vec![array![1.0, 0.0], array![0.5, 0.5], array![0.0, 1.0]];
737
738 let neighborhoods = compute_neighborhoods(&vectors, 2);
739 assert_eq!(neighborhoods.len(), 3);
740 for n in &neighborhoods {
742 assert_eq!(n.len(), 2);
743 }
744 }
745
746 #[test]
747 fn test_scalarization_tchebycheff() {
748 let config = MultiObjectiveConfig {
749 population_size: 10,
750 ..Default::default()
751 };
752 let moead = MOEAD::with_config(config, 2, 2, ScalarizationMethod::Tchebycheff);
753
754 let objectives = array![2.0, 3.0];
755 let weight = array![0.5, 0.5];
756 let val = moead.scalar_value(&objectives, &weight);
757 assert!(val.is_finite() || val.is_infinite()); }
762
763 #[test]
764 fn test_moead_name() {
765 let moead = MOEAD::new(50, 2, 3);
766 assert_eq!(moead.name(), "MOEA/D");
767 }
768
769 #[test]
770 fn test_binomial_coefficient() {
771 assert_eq!(binomial_coefficient(5, 2), 10);
772 assert_eq!(binomial_coefficient(10, 3), 120);
773 assert_eq!(binomial_coefficient(4, 0), 1);
774 assert_eq!(binomial_coefficient(0, 0), 1);
775 }
776}