scirs2_optimize/multi_objective/solutions/
mod.rs

1//! Multi-objective solution representations and utilities
2//!
3//! This module provides core data structures for multi-objective optimization:
4//! - Solution representation with objectives and constraints
5//! - Population management utilities
6//! - Pareto front analysis
7//! - Solution comparison and ranking
8
9use ndarray::{Array1, ArrayView1};
10use serde::{Deserialize, Serialize};
11use std::cmp::Ordering;
12use std::collections::HashMap;
13
14/// Type alias for backward compatibility
15pub type Solution = MultiObjectiveSolution;
16
17/// Represents a solution in multi-objective optimization
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct MultiObjectiveSolution {
20    /// Decision variables
21    pub variables: Array1<f64>,
22    /// Objective function values
23    pub objectives: Array1<f64>,
24    /// Constraint violation (0.0 if feasible)
25    pub constraint_violation: f64,
26    /// Rank in the population (for NSGA-II/III)
27    pub rank: usize,
28    /// Crowding distance (for NSGA-II)
29    pub crowding_distance: f64,
30    /// Additional metadata
31    pub metadata: HashMap<String, f64>,
32}
33
34impl MultiObjectiveSolution {
35    /// Create a new solution
36    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    /// Create a new solution with constraint violation
48    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    /// Check if this solution dominates another
64    pub fn dominates(&self, other: &Self) -> bool {
65        // First check constraint violations
66        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        // If both are equally feasible/infeasible, compare objectives
74        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; // Assuming minimization
79            }
80            if obj1 < obj2 {
81                at_least_one_better = true;
82            }
83        }
84
85        at_least_one_better
86    }
87
88    /// Check if this solution is dominated by another
89    pub fn is_dominated_by(&self, other: &Self) -> bool {
90        other.dominates(self)
91    }
92
93    /// Check if solutions are non-dominated with respect to each other
94    pub fn is_non_dominated_with(&self, other: &Self) -> bool {
95        !self.dominates(other) && !other.dominates(self)
96    }
97
98    /// Calculate distance to another solution in objective space
99    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    /// Calculate distance to another solution in variable space
109    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    /// Get number of objectives
119    pub fn n_objectives(&self) -> usize {
120        self.objectives.len()
121    }
122
123    /// Get number of variables
124    pub fn n_variables(&self) -> usize {
125        self.variables.len()
126    }
127
128    /// Check if solution is feasible
129    pub fn is_feasible(&self) -> bool {
130        self.constraint_violation <= 1e-10
131    }
132
133    /// Set metadata value
134    pub fn set_metadata(&mut self, key: String, value: f64) {
135        self.metadata.insert(key, value);
136    }
137
138    /// Get metadata value
139    pub fn get_metadata(&self, key: &str) -> Option<f64> {
140        self.metadata.get(key).copied()
141    }
142
143    /// Clone with new objectives (useful for constraint handling)
144    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    /// Clone with new variables
156    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        // Compare by rank first, then by crowding distance
177        match self.rank.cmp(&other.rank) {
178            Ordering::Equal => {
179                // Higher crowding distance is better (reversed comparison)
180                other.crowding_distance.partial_cmp(&self.crowding_distance)
181            }
182            other_order => Some(other_order),
183        }
184    }
185}
186
187/// Result of multi-objective optimization
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct MultiObjectiveResult {
190    /// Pareto front solutions
191    pub pareto_front: Vec<MultiObjectiveSolution>,
192    /// All final population solutions
193    pub population: Vec<MultiObjectiveSolution>,
194    /// Number of function evaluations
195    pub n_evaluations: usize,
196    /// Number of generations/iterations
197    pub n_generations: usize,
198    /// Success flag
199    pub success: bool,
200    /// Convergence message
201    pub message: String,
202    /// Hypervolume indicator (if reference point provided)
203    pub hypervolume: Option<f64>,
204    /// Additional metrics
205    pub metrics: OptimizationMetrics,
206}
207
208impl MultiObjectiveResult {
209    /// Create a new result
210    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    /// Create a failed result
229    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    /// Get best solution for a specific objective
243    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    /// Get solution closest to ideal point
252    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    /// Get number of solutions in Pareto front
271    pub fn pareto_front_size(&self) -> usize {
272        self.pareto_front.len()
273    }
274
275    /// Check if result contains feasible solutions
276    pub fn has_feasible_solutions(&self) -> bool {
277        self.pareto_front.iter().any(|sol| sol.is_feasible())
278    }
279
280    /// Get all feasible solutions
281    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/// Optimization metrics for tracking performance
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct OptimizationMetrics {
292    /// Convergence history (hypervolume over generations)
293    pub convergence_history: Vec<f64>,
294    /// Diversity metrics over generations
295    pub diversity_history: Vec<f64>,
296    /// Average objective values over generations
297    pub average_objectives: Vec<Array1<f64>>,
298    /// Best objective values over generations
299    pub best_objectives: Vec<Array1<f64>>,
300    /// Population statistics
301    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/// Population statistics
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct PopulationStatistics {
319    /// Mean objective values
320    pub mean_objectives: Array1<f64>,
321    /// Standard deviation of objectives
322    pub std_objectives: Array1<f64>,
323    /// Minimum objective values
324    pub min_objectives: Array1<f64>,
325    /// Maximum objective values
326    pub max_objectives: Array1<f64>,
327    /// Feasibility ratio
328    pub feasibility_ratio: f64,
329    /// Average constraint violation
330    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/// Population management utilities
347#[derive(Debug, Clone)]
348pub struct Population {
349    solutions: Vec<MultiObjectiveSolution>,
350}
351
352impl Population {
353    /// Create a new empty population
354    pub fn new() -> Self {
355        Self { solutions: vec![] }
356    }
357
358    /// Create a new population with specified capacity
359    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    /// Create population from solutions
370    pub fn from_solutions(solutions: Vec<MultiObjectiveSolution>) -> Self {
371        Self { solutions }
372    }
373
374    /// Add a solution to the population
375    pub fn add(&mut self, solution: MultiObjectiveSolution) {
376        self.solutions.push(solution);
377    }
378
379    /// Get all solutions
380    pub fn solutions(&self) -> &[MultiObjectiveSolution] {
381        &self.solutions
382    }
383
384    /// Get mutable reference to solutions
385    pub fn solutions_mut(&mut self) -> &mut Vec<MultiObjectiveSolution> {
386        &mut self.solutions
387    }
388
389    /// Get population size
390    pub fn size(&self) -> usize {
391        self.solutions.len()
392    }
393
394    /// Check if population is empty
395    pub fn is_empty(&self) -> bool {
396        self.solutions.is_empty()
397    }
398
399    /// Clear the population
400    pub fn clear(&mut self) {
401        self.solutions.clear();
402    }
403
404    /// Extract Pareto front from population
405    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            // Check if candidate is dominated by any existing solution in Pareto front
412            for existing in &pareto_front {
413                if existing.dominates(candidate) {
414                    is_dominated = true;
415                    break;
416                }
417            }
418
419            if !is_dominated {
420                // Remove solutions from Pareto front that are dominated by candidate
421                pareto_front.retain(|existing| !candidate.dominates(existing));
422                pareto_front.push(candidate.clone());
423            }
424        }
425
426        pareto_front
427    }
428
429    /// Perform non-dominated sorting (for NSGA-II)
430    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        // Calculate domination relationships
437        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        // Find first front
450        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 &current_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    /// Calculate crowding distances for solutions in the same front
482    pub fn calculate_crowding_distances(&mut self, front_indices: &[usize]) {
483        let front_size = front_indices.len();
484        if front_size <= 2 {
485            // Set infinite distance for boundary solutions
486            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        // Initialize distances to zero
495        for &i in front_indices {
496            self.solutions[i].crowding_distance = 0.0;
497        }
498
499        // Calculate distance for each objective
500        for obj in 0..n_objectives {
501            // Sort by current objective
502            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            // Set boundary solutions to infinite distance
510            self.solutions[sorted_indices[0]].crowding_distance = f64::INFINITY;
511            self.solutions[sorted_indices[front_size - 1]].crowding_distance = f64::INFINITY;
512
513            // Calculate objective range
514            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                // Calculate distances for intermediate solutions
520                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    /// Select best solutions using NSGA-II crowded comparison
533    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        // Perform non-dominated sorting
539        let fronts = self.non_dominated_sort();
540
541        // Calculate crowding distances for each front
542        for front in &fronts {
543            self.calculate_crowding_distances(front);
544        }
545
546        let mut selected = Vec::new();
547
548        // Add complete fronts until we reach capacity
549        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                // Add partial front based on crowding distance
556                let remaining = target_size - selected.len();
557                let mut front_solutions: Vec<_> =
558                    front.iter().map(|&i| &self.solutions[i]).collect();
559
560                // Sort by crowding distance (descending)
561                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    /// Calculate population statistics
578    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        // Calculate means, mins, maxs
594        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        // Calculate standard deviations
610        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)); // sol1 is better in both objectives
660        assert!(!sol2.dominates(&sol1));
661        assert!(sol1.is_non_dominated_with(&sol3)); // Neither dominates the other
662    }
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)); // Feasible dominates infeasible
672        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])); // Dominated
682
683        let pareto_front = population.extract_pareto_front();
684        assert_eq!(pareto_front.len(), 3); // First three solutions are non-dominated
685    }
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])); // Dominated
694
695        let fronts = population.non_dominated_sort();
696
697        assert_eq!(fronts.len(), 2); // Two fronts
698        assert_eq!(fronts[0].len(), 3); // First front has 3 solutions
699        assert_eq!(fronts[1].len(), 1); // Second front has 1 solution
700
701        // Check ranks
702        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        // Boundary solutions should have infinite distance
721        assert_eq!(population.solutions[0].crowding_distance, f64::INFINITY);
722        assert_eq!(population.solutions[2].crowding_distance, f64::INFINITY);
723        // Middle solution should have finite distance
724        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); // (1+2+3)/3
738        assert_eq!(stats.mean_objectives[1], 2.0); // (3+2+1)/3
739        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); // All feasible
742    }
743}