1use ndarray::{Array1, ArrayView1};
10use serde::{Deserialize, Serialize};
11use std::cmp::Ordering;
12use std::collections::HashMap;
13
14pub type Solution = MultiObjectiveSolution;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct MultiObjectiveSolution {
20 pub variables: Array1<f64>,
22 pub objectives: Array1<f64>,
24 pub constraint_violation: f64,
26 pub rank: usize,
28 pub crowding_distance: f64,
30 pub metadata: HashMap<String, f64>,
32}
33
34impl MultiObjectiveSolution {
35 pub fn new(variables: Array1<f64>, objectives: Array1<f64>) -> Self {
37 Self {
38 variables,
39 objectives,
40 constraint_violation: 0.0,
41 rank: 0,
42 crowding_distance: 0.0,
43 metadata: HashMap::new(),
44 }
45 }
46
47 pub fn new_with_constraints(
49 variables: Array1<f64>,
50 objectives: Array1<f64>,
51 constraint_violation: f64,
52 ) -> Self {
53 Self {
54 variables,
55 objectives,
56 constraint_violation,
57 rank: 0,
58 crowding_distance: 0.0,
59 metadata: HashMap::new(),
60 }
61 }
62
63 pub fn dominates(&self, other: &Self) -> bool {
65 if self.constraint_violation < other.constraint_violation {
67 return true;
68 }
69 if self.constraint_violation > other.constraint_violation {
70 return false;
71 }
72
73 let mut at_least_one_better = false;
75
76 for (obj1, obj2) in self.objectives.iter().zip(other.objectives.iter()) {
77 if obj1 > obj2 {
78 return false; }
80 if obj1 < obj2 {
81 at_least_one_better = true;
82 }
83 }
84
85 at_least_one_better
86 }
87
88 pub fn is_dominated_by(&self, other: &Self) -> bool {
90 other.dominates(self)
91 }
92
93 pub fn is_non_dominated_with(&self, other: &Self) -> bool {
95 !self.dominates(other) && !other.dominates(self)
96 }
97
98 pub fn objective_distance(&self, other: &Self) -> f64 {
100 self.objectives
101 .iter()
102 .zip(other.objectives.iter())
103 .map(|(a, b)| (a - b).powi(2))
104 .sum::<f64>()
105 .sqrt()
106 }
107
108 pub fn variable_distance(&self, other: &Self) -> f64 {
110 self.variables
111 .iter()
112 .zip(other.variables.iter())
113 .map(|(a, b)| (a - b).powi(2))
114 .sum::<f64>()
115 .sqrt()
116 }
117
118 pub fn n_objectives(&self) -> usize {
120 self.objectives.len()
121 }
122
123 pub fn n_variables(&self) -> usize {
125 self.variables.len()
126 }
127
128 pub fn is_feasible(&self) -> bool {
130 self.constraint_violation <= 1e-10
131 }
132
133 pub fn set_metadata(&mut self, key: String, value: f64) {
135 self.metadata.insert(key, value);
136 }
137
138 pub fn get_metadata(&self, key: &str) -> Option<f64> {
140 self.metadata.get(key).copied()
141 }
142
143 pub fn with_objectives(&self, objectives: Array1<f64>) -> Self {
145 Self {
146 variables: self.variables.clone(),
147 objectives,
148 constraint_violation: self.constraint_violation,
149 rank: self.rank,
150 crowding_distance: self.crowding_distance,
151 metadata: self.metadata.clone(),
152 }
153 }
154
155 pub fn with_variables(&self, variables: Array1<f64>) -> Self {
157 Self {
158 variables,
159 objectives: self.objectives.clone(),
160 constraint_violation: self.constraint_violation,
161 rank: self.rank,
162 crowding_distance: self.crowding_distance,
163 metadata: self.metadata.clone(),
164 }
165 }
166}
167
168impl PartialEq for MultiObjectiveSolution {
169 fn eq(&self, other: &Self) -> bool {
170 self.variables == other.variables && self.objectives == other.objectives
171 }
172}
173
174impl PartialOrd for MultiObjectiveSolution {
175 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
176 match self.rank.cmp(&other.rank) {
178 Ordering::Equal => {
179 other.crowding_distance.partial_cmp(&self.crowding_distance)
181 }
182 other_order => Some(other_order),
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct MultiObjectiveResult {
190 pub pareto_front: Vec<MultiObjectiveSolution>,
192 pub population: Vec<MultiObjectiveSolution>,
194 pub n_evaluations: usize,
196 pub n_generations: usize,
198 pub success: bool,
200 pub message: String,
202 pub hypervolume: Option<f64>,
204 pub metrics: OptimizationMetrics,
206}
207
208impl MultiObjectiveResult {
209 pub fn new(
211 pareto_front: Vec<MultiObjectiveSolution>,
212 population: Vec<MultiObjectiveSolution>,
213 n_evaluations: usize,
214 n_generations: usize,
215 ) -> Self {
216 Self {
217 pareto_front,
218 population,
219 n_evaluations,
220 n_generations,
221 success: true,
222 message: "Optimization completed successfully".to_string(),
223 hypervolume: None,
224 metrics: OptimizationMetrics::default(),
225 }
226 }
227
228 pub fn failed(message: String, n_evaluations: usize, n_generations: usize) -> Self {
230 Self {
231 pareto_front: vec![],
232 population: vec![],
233 n_evaluations,
234 n_generations,
235 success: false,
236 message,
237 hypervolume: None,
238 metrics: OptimizationMetrics::default(),
239 }
240 }
241
242 pub fn best_for_objective(&self, objective_index: usize) -> Option<&MultiObjectiveSolution> {
244 self.pareto_front.iter().min_by(|a, b| {
245 a.objectives[objective_index]
246 .partial_cmp(&b.objectives[objective_index])
247 .unwrap_or(Ordering::Equal)
248 })
249 }
250
251 pub fn closest_to_ideal(&self, ideal_point: &Array1<f64>) -> Option<&MultiObjectiveSolution> {
253 self.pareto_front.iter().min_by(|a, b| {
254 let dist_a = a
255 .objectives
256 .iter()
257 .zip(ideal_point.iter())
258 .map(|(obj, ideal)| (obj - ideal).powi(2))
259 .sum::<f64>();
260 let dist_b = b
261 .objectives
262 .iter()
263 .zip(ideal_point.iter())
264 .map(|(obj, ideal)| (obj - ideal).powi(2))
265 .sum::<f64>();
266 dist_a.partial_cmp(&dist_b).unwrap_or(Ordering::Equal)
267 })
268 }
269
270 pub fn pareto_front_size(&self) -> usize {
272 self.pareto_front.len()
273 }
274
275 pub fn has_feasible_solutions(&self) -> bool {
277 self.pareto_front.iter().any(|sol| sol.is_feasible())
278 }
279
280 pub fn feasible_solutions(&self) -> Vec<&MultiObjectiveSolution> {
282 self.pareto_front
283 .iter()
284 .filter(|sol| sol.is_feasible())
285 .collect()
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct OptimizationMetrics {
292 pub convergence_history: Vec<f64>,
294 pub diversity_history: Vec<f64>,
296 pub average_objectives: Vec<Array1<f64>>,
298 pub best_objectives: Vec<Array1<f64>>,
300 pub population_stats: PopulationStatistics,
302}
303
304impl Default for OptimizationMetrics {
305 fn default() -> Self {
306 Self {
307 convergence_history: vec![],
308 diversity_history: vec![],
309 average_objectives: vec![],
310 best_objectives: vec![],
311 population_stats: PopulationStatistics::default(),
312 }
313 }
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct PopulationStatistics {
319 pub mean_objectives: Array1<f64>,
321 pub std_objectives: Array1<f64>,
323 pub min_objectives: Array1<f64>,
325 pub max_objectives: Array1<f64>,
327 pub feasibility_ratio: f64,
329 pub avg_constraint_violation: f64,
331}
332
333impl Default for PopulationStatistics {
334 fn default() -> Self {
335 Self {
336 mean_objectives: Array1::zeros(0),
337 std_objectives: Array1::zeros(0),
338 min_objectives: Array1::zeros(0),
339 max_objectives: Array1::zeros(0),
340 feasibility_ratio: 0.0,
341 avg_constraint_violation: 0.0,
342 }
343 }
344}
345
346#[derive(Debug, Clone)]
348pub struct Population {
349 solutions: Vec<MultiObjectiveSolution>,
350}
351
352impl Population {
353 pub fn new() -> Self {
355 Self { solutions: vec![] }
356 }
357
358 pub fn with_capacity(
360 population_size: usize,
361 _n_objectives: usize,
362 _n_variables: usize,
363 ) -> Self {
364 Self {
365 solutions: Vec::with_capacity(population_size),
366 }
367 }
368
369 pub fn from_solutions(solutions: Vec<MultiObjectiveSolution>) -> Self {
371 Self { solutions }
372 }
373
374 pub fn add(&mut self, solution: MultiObjectiveSolution) {
376 self.solutions.push(solution);
377 }
378
379 pub fn solutions(&self) -> &[MultiObjectiveSolution] {
381 &self.solutions
382 }
383
384 pub fn solutions_mut(&mut self) -> &mut Vec<MultiObjectiveSolution> {
386 &mut self.solutions
387 }
388
389 pub fn size(&self) -> usize {
391 self.solutions.len()
392 }
393
394 pub fn is_empty(&self) -> bool {
396 self.solutions.is_empty()
397 }
398
399 pub fn clear(&mut self) {
401 self.solutions.clear();
402 }
403
404 pub fn extract_pareto_front(&self) -> Vec<MultiObjectiveSolution> {
406 let mut pareto_front: Vec<MultiObjectiveSolution> = Vec::new();
407
408 for candidate in &self.solutions {
409 let mut is_dominated = false;
410
411 for existing in &pareto_front {
413 if existing.dominates(candidate) {
414 is_dominated = true;
415 break;
416 }
417 }
418
419 if !is_dominated {
420 pareto_front.retain(|existing| !candidate.dominates(existing));
422 pareto_front.push(candidate.clone());
423 }
424 }
425
426 pareto_front
427 }
428
429 pub fn non_dominated_sort(&mut self) -> Vec<Vec<usize>> {
431 let n = self.solutions.len();
432 let mut fronts = Vec::new();
433 let mut domination_counts = vec![0; n];
434 let mut dominated_solutions: Vec<Vec<usize>> = vec![Vec::new(); n];
435
436 for i in 0..n {
438 for j in 0..n {
439 if i != j {
440 if self.solutions[i].dominates(&self.solutions[j]) {
441 dominated_solutions[i].push(j);
442 } else if self.solutions[j].dominates(&self.solutions[i]) {
443 domination_counts[i] += 1;
444 }
445 }
446 }
447 }
448
449 let mut current_front = Vec::new();
451 for i in 0..n {
452 if domination_counts[i] == 0 {
453 self.solutions[i].rank = 0;
454 current_front.push(i);
455 }
456 }
457
458 let mut front_number = 0;
459
460 while !current_front.is_empty() {
461 fronts.push(current_front.clone());
462 let mut next_front = Vec::new();
463
464 for &i in ¤t_front {
465 for &j in &dominated_solutions[i] {
466 domination_counts[j] -= 1;
467 if domination_counts[j] == 0 {
468 self.solutions[j].rank = front_number + 1;
469 next_front.push(j);
470 }
471 }
472 }
473
474 current_front = next_front;
475 front_number += 1;
476 }
477
478 fronts
479 }
480
481 pub fn calculate_crowding_distances(&mut self, front_indices: &[usize]) {
483 let front_size = front_indices.len();
484 if front_size <= 2 {
485 for &i in front_indices {
487 self.solutions[i].crowding_distance = f64::INFINITY;
488 }
489 return;
490 }
491
492 let n_objectives = self.solutions[front_indices[0]].n_objectives();
493
494 for &i in front_indices {
496 self.solutions[i].crowding_distance = 0.0;
497 }
498
499 for obj in 0..n_objectives {
501 let mut sorted_indices = front_indices.to_vec();
503 sorted_indices.sort_by(|&a, &b| {
504 self.solutions[a].objectives[obj]
505 .partial_cmp(&self.solutions[b].objectives[obj])
506 .unwrap_or(Ordering::Equal)
507 });
508
509 self.solutions[sorted_indices[0]].crowding_distance = f64::INFINITY;
511 self.solutions[sorted_indices[front_size - 1]].crowding_distance = f64::INFINITY;
512
513 let obj_min = self.solutions[sorted_indices[0]].objectives[obj];
515 let obj_max = self.solutions[sorted_indices[front_size - 1]].objectives[obj];
516 let obj_range = obj_max - obj_min;
517
518 if obj_range > 0.0 {
519 for i in 1..front_size - 1 {
521 if self.solutions[sorted_indices[i]].crowding_distance != f64::INFINITY {
522 let distance = (self.solutions[sorted_indices[i + 1]].objectives[obj]
523 - self.solutions[sorted_indices[i - 1]].objectives[obj])
524 / obj_range;
525 self.solutions[sorted_indices[i]].crowding_distance += distance;
526 }
527 }
528 }
529 }
530 }
531
532 pub fn select_best(&mut self, target_size: usize) -> Vec<MultiObjectiveSolution> {
534 if self.solutions.len() <= target_size {
535 return self.solutions.clone();
536 }
537
538 let fronts = self.non_dominated_sort();
540
541 for front in &fronts {
543 self.calculate_crowding_distances(front);
544 }
545
546 let mut selected = Vec::new();
547
548 for front in &fronts {
550 if selected.len() + front.len() <= target_size {
551 for &i in front {
552 selected.push(self.solutions[i].clone());
553 }
554 } else {
555 let remaining = target_size - selected.len();
557 let mut front_solutions: Vec<_> =
558 front.iter().map(|&i| &self.solutions[i]).collect();
559
560 front_solutions.sort_by(|a, b| {
562 b.crowding_distance
563 .partial_cmp(&a.crowding_distance)
564 .unwrap_or(Ordering::Equal)
565 });
566
567 for solution in front_solutions.iter().take(remaining) {
568 selected.push((*solution).clone());
569 }
570 break;
571 }
572 }
573
574 selected
575 }
576
577 pub fn calculate_statistics(&self) -> PopulationStatistics {
579 if self.solutions.is_empty() {
580 return PopulationStatistics::default();
581 }
582
583 let n_objectives = self.solutions[0].n_objectives();
584 let n_solutions = self.solutions.len();
585
586 let mut mean_objectives = Array1::zeros(n_objectives);
587 let mut min_objectives = Array1::from_elem(n_objectives, f64::INFINITY);
588 let mut max_objectives = Array1::from_elem(n_objectives, f64::NEG_INFINITY);
589
590 let mut feasible_count = 0;
591 let mut total_violation = 0.0;
592
593 for solution in &self.solutions {
595 for (i, &obj) in solution.objectives.iter().enumerate() {
596 mean_objectives[i] += obj;
597 min_objectives[i] = min_objectives[i].min(obj);
598 max_objectives[i] = max_objectives[i].max(obj);
599 }
600
601 if solution.is_feasible() {
602 feasible_count += 1;
603 }
604 total_violation += solution.constraint_violation;
605 }
606
607 mean_objectives /= n_solutions as f64;
608
609 let mut std_objectives = Array1::zeros(n_objectives);
611 for solution in &self.solutions {
612 for (i, &obj) in solution.objectives.iter().enumerate() {
613 let diff = obj - mean_objectives[i];
614 std_objectives[i] += diff * diff;
615 }
616 }
617 std_objectives = std_objectives.mapv(|x: f64| (x / n_solutions as f64).sqrt());
618
619 PopulationStatistics {
620 mean_objectives,
621 std_objectives,
622 min_objectives,
623 max_objectives,
624 feasibility_ratio: feasible_count as f64 / n_solutions as f64,
625 avg_constraint_violation: total_violation / n_solutions as f64,
626 }
627 }
628}
629
630impl Default for Population {
631 fn default() -> Self {
632 Self::new()
633 }
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639 use ndarray::array;
640
641 #[test]
642 fn test_solution_creation() {
643 let variables = array![1.0, 2.0, 3.0];
644 let objectives = array![4.0, 5.0];
645 let solution = MultiObjectiveSolution::new(variables.clone(), objectives.clone());
646
647 assert_eq!(solution.variables, variables);
648 assert_eq!(solution.objectives, objectives);
649 assert_eq!(solution.constraint_violation, 0.0);
650 assert!(solution.is_feasible());
651 }
652
653 #[test]
654 fn test_domination() {
655 let sol1 = MultiObjectiveSolution::new(array![1.0], array![1.0, 2.0]);
656 let sol2 = MultiObjectiveSolution::new(array![2.0], array![2.0, 3.0]);
657 let sol3 = MultiObjectiveSolution::new(array![3.0], array![0.5, 3.5]);
658
659 assert!(sol1.dominates(&sol2)); assert!(!sol2.dominates(&sol1));
661 assert!(sol1.is_non_dominated_with(&sol3)); }
663
664 #[test]
665 fn test_constraint_domination() {
666 let feasible =
667 MultiObjectiveSolution::new_with_constraints(array![1.0], array![2.0, 3.0], 0.0);
668 let infeasible =
669 MultiObjectiveSolution::new_with_constraints(array![2.0], array![1.0, 2.0], 1.0);
670
671 assert!(feasible.dominates(&infeasible)); assert!(!infeasible.dominates(&feasible));
673 }
674
675 #[test]
676 fn test_population_pareto_front() {
677 let mut population = Population::new();
678 population.add(MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]));
679 population.add(MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]));
680 population.add(MultiObjectiveSolution::new(array![3.0], array![3.0, 1.0]));
681 population.add(MultiObjectiveSolution::new(array![4.0], array![2.5, 2.5])); let pareto_front = population.extract_pareto_front();
684 assert_eq!(pareto_front.len(), 3); }
686
687 #[test]
688 fn test_non_dominated_sorting() {
689 let mut population = Population::new();
690 population.add(MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]));
691 population.add(MultiObjectiveSolution::new(array![2.0], array![3.0, 1.0]));
692 population.add(MultiObjectiveSolution::new(array![3.0], array![2.0, 2.0]));
693 population.add(MultiObjectiveSolution::new(array![4.0], array![4.0, 4.0])); let fronts = population.non_dominated_sort();
696
697 assert_eq!(fronts.len(), 2); assert_eq!(fronts[0].len(), 3); assert_eq!(fronts[1].len(), 1); for &i in &fronts[0] {
703 assert_eq!(population.solutions[i].rank, 0);
704 }
705 for &i in &fronts[1] {
706 assert_eq!(population.solutions[i].rank, 1);
707 }
708 }
709
710 #[test]
711 fn test_crowding_distance() {
712 let mut population = Population::new();
713 population.add(MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]));
714 population.add(MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]));
715 population.add(MultiObjectiveSolution::new(array![3.0], array![3.0, 1.0]));
716
717 let front = vec![0, 1, 2];
718 population.calculate_crowding_distances(&front);
719
720 assert_eq!(population.solutions[0].crowding_distance, f64::INFINITY);
722 assert_eq!(population.solutions[2].crowding_distance, f64::INFINITY);
723 assert!(population.solutions[1].crowding_distance.is_finite());
725 assert!(population.solutions[1].crowding_distance > 0.0);
726 }
727
728 #[test]
729 fn test_population_statistics() {
730 let mut population = Population::new();
731 population.add(MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]));
732 population.add(MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]));
733 population.add(MultiObjectiveSolution::new(array![3.0], array![3.0, 1.0]));
734
735 let stats = population.calculate_statistics();
736
737 assert_eq!(stats.mean_objectives[0], 2.0); assert_eq!(stats.mean_objectives[1], 2.0); assert_eq!(stats.min_objectives[0], 1.0);
740 assert_eq!(stats.max_objectives[0], 3.0);
741 assert_eq!(stats.feasibility_ratio, 1.0); }
743}