Skip to main content

quantrs2_anneal/meta_learning/
multi_objective.rs

1//! Multi-Objective Optimization for Meta-Learning
2//!
3//! This module contains all Multi-Objective Optimization types and implementations
4//! used by the meta-learning optimization system.
5
6use super::config::{
7    ConstraintHandling, FrontierUpdateStrategy, MultiObjectiveConfig, OptimizationConfiguration,
8    OptimizationObjective, ParetoFrontierConfig, ScalarizationMethod,
9};
10use crate::applications::ApplicationResult;
11use std::collections::{HashMap, VecDeque};
12use std::time::{Duration, Instant};
13
14/// Multi-objective optimizer
15pub struct MultiObjectiveOptimizer {
16    /// Configuration
17    pub config: MultiObjectiveConfig,
18    /// Pareto frontier
19    pub pareto_frontier: ParetoFrontier,
20    /// Scalarization methods
21    pub scalarizers: Vec<Scalarizer>,
22    /// Constraint handlers
23    pub constraint_handlers: Vec<ConstraintHandler>,
24    /// Decision maker
25    pub decision_maker: DecisionMaker,
26}
27
28/// Pareto frontier representation
29#[derive(Debug)]
30pub struct ParetoFrontier {
31    /// Non-dominated solutions
32    pub solutions: Vec<MultiObjectiveSolution>,
33    /// Frontier statistics
34    pub statistics: FrontierStatistics,
35    /// Update history
36    pub update_history: VecDeque<FrontierUpdate>,
37}
38
39/// Multi-objective solution
40#[derive(Debug, Clone)]
41pub struct MultiObjectiveSolution {
42    /// Solution identifier
43    pub id: String,
44    /// Objective values
45    pub objective_values: Vec<f64>,
46    /// Decision variables
47    pub decision_variables: OptimizationConfiguration,
48    /// Dominance rank
49    pub dominance_rank: usize,
50    /// Crowding distance
51    pub crowding_distance: f64,
52}
53
54/// Frontier statistics
55#[derive(Debug, Clone)]
56pub struct FrontierStatistics {
57    /// Frontier size
58    pub size: usize,
59    /// Hypervolume
60    pub hypervolume: f64,
61    /// Spread
62    pub spread: f64,
63    /// Convergence metric
64    pub convergence: f64,
65    /// Coverage
66    pub coverage: f64,
67}
68
69/// Frontier update
70#[derive(Debug, Clone)]
71pub struct FrontierUpdate {
72    /// Update timestamp
73    pub timestamp: Instant,
74    /// Solutions added
75    pub solutions_added: Vec<String>,
76    /// Solutions removed
77    pub solutions_removed: Vec<String>,
78    /// Update reason
79    pub reason: UpdateReason,
80}
81
82/// Update reasons
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum UpdateReason {
85    /// New non-dominated solution
86    NewNonDominated,
87    /// Dominated solution removal
88    DominatedRemoval,
89    /// Capacity limit reached
90    CapacityLimit,
91    /// Quality improvement
92    QualityImprovement,
93}
94
95/// Scalarization function
96#[derive(Debug)]
97pub struct Scalarizer {
98    /// Method used
99    pub method: ScalarizationMethod,
100    /// Weights or preferences
101    pub weights: Vec<f64>,
102    /// Reference point
103    pub reference_point: Option<Vec<f64>>,
104    /// Parameters
105    pub parameters: HashMap<String, f64>,
106}
107
108/// Constraint handler
109#[derive(Debug)]
110pub struct ConstraintHandler {
111    /// Handling method
112    pub method: ConstraintHandling,
113    /// Constraints
114    pub constraints: Vec<Constraint>,
115    /// Penalty parameters
116    pub penalty_parameters: HashMap<String, f64>,
117}
118
119/// Constraint definition
120#[derive(Debug, Clone)]
121pub struct Constraint {
122    /// Constraint type
123    pub constraint_type: ConstraintType,
124    /// Constraint function
125    pub function: String,
126    /// Bounds
127    pub bounds: (f64, f64),
128    /// Tolerance
129    pub tolerance: f64,
130}
131
132/// Constraint types
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum ConstraintType {
135    /// Equality constraint
136    Equality,
137    /// Inequality constraint
138    Inequality,
139    /// Box constraint
140    Box,
141    /// Linear constraint
142    Linear,
143    /// Nonlinear constraint
144    Nonlinear,
145}
146
147/// Decision maker for multi-objective problems
148#[derive(Debug)]
149pub struct DecisionMaker {
150    /// Decision strategy
151    pub strategy: DecisionStrategy,
152    /// Preference information
153    pub preferences: UserPreferences,
154    /// Decision history
155    pub decision_history: VecDeque<Decision>,
156}
157
158/// Decision strategies
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum DecisionStrategy {
161    /// Interactive decision making
162    Interactive,
163    /// A priori preferences
164    APriori,
165    /// A posteriori analysis
166    APosteriori,
167    /// Progressive articulation
168    Progressive,
169    /// Automated decision
170    Automated,
171}
172
173/// User preferences
174#[derive(Debug, Clone)]
175pub struct UserPreferences {
176    /// Objective weights
177    pub objective_weights: Vec<f64>,
178    /// Acceptable trade-offs
179    pub trade_offs: HashMap<String, f64>,
180    /// Constraints
181    pub user_constraints: Vec<Constraint>,
182    /// Preference functions
183    pub preference_functions: Vec<PreferenceFunction>,
184}
185
186/// Preference function
187#[derive(Debug, Clone)]
188pub struct PreferenceFunction {
189    /// Function type
190    pub function_type: PreferenceFunctionType,
191    /// Parameters
192    pub parameters: Vec<f64>,
193    /// Applicable objectives
194    pub objectives: Vec<usize>,
195}
196
197/// Types of preference functions
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum PreferenceFunctionType {
200    /// Linear preference
201    Linear,
202    /// Exponential preference
203    Exponential,
204    /// Logarithmic preference
205    Logarithmic,
206    /// Threshold-based
207    Threshold,
208    /// Custom function
209    Custom(String),
210}
211
212/// Decision record
213#[derive(Debug, Clone)]
214pub struct Decision {
215    /// Decision timestamp
216    pub timestamp: Instant,
217    /// Selected solution
218    pub selected_solution: String,
219    /// Decision rationale
220    pub rationale: String,
221    /// Confidence level
222    pub confidence: f64,
223    /// User feedback
224    pub user_feedback: Option<f64>,
225}
226
227impl MultiObjectiveOptimizer {
228    #[must_use]
229    pub fn new(config: MultiObjectiveConfig) -> Self {
230        Self {
231            config,
232            pareto_frontier: ParetoFrontier {
233                solutions: Vec::new(),
234                statistics: FrontierStatistics {
235                    size: 0,
236                    hypervolume: 0.0,
237                    spread: 0.0,
238                    convergence: 0.0,
239                    coverage: 0.0,
240                },
241                update_history: VecDeque::new(),
242            },
243            scalarizers: Vec::new(),
244            constraint_handlers: Vec::new(),
245            decision_maker: DecisionMaker {
246                strategy: DecisionStrategy::Automated,
247                preferences: UserPreferences {
248                    objective_weights: vec![0.5, 0.3, 0.2],
249                    trade_offs: HashMap::new(),
250                    user_constraints: Vec::new(),
251                    preference_functions: Vec::new(),
252                },
253                decision_history: VecDeque::new(),
254            },
255        }
256    }
257
258    /// Add solution to Pareto frontier
259    pub fn add_solution(&mut self, solution: MultiObjectiveSolution) -> ApplicationResult<bool> {
260        // Check if solution is non-dominated
261        let is_non_dominated = self.is_non_dominated(&solution);
262
263        if is_non_dominated {
264            // Remove dominated solutions
265            let solutions_to_keep: Vec<_> = self
266                .pareto_frontier
267                .solutions
268                .iter()
269                .filter(|existing| !self.dominates(&solution, existing))
270                .cloned()
271                .collect();
272            self.pareto_frontier.solutions = solutions_to_keep;
273
274            // Add new solution
275            self.pareto_frontier.solutions.push(solution.clone());
276
277            // Update statistics
278            self.update_frontier_statistics();
279
280            // Record update
281            let update = FrontierUpdate {
282                timestamp: Instant::now(),
283                solutions_added: vec![solution.id],
284                solutions_removed: Vec::new(),
285                reason: UpdateReason::NewNonDominated,
286            };
287            self.pareto_frontier.update_history.push_back(update);
288
289            // Limit history size
290            if self.pareto_frontier.update_history.len() > 1000 {
291                self.pareto_frontier.update_history.pop_front();
292            }
293
294            Ok(true)
295        } else {
296            Ok(false)
297        }
298    }
299
300    /// Check if solution is non-dominated
301    fn is_non_dominated(&self, solution: &MultiObjectiveSolution) -> bool {
302        for existing in &self.pareto_frontier.solutions {
303            if self.dominates(existing, solution) {
304                return false;
305            }
306        }
307        true
308    }
309
310    /// Check if solution1 dominates solution2
311    fn dominates(
312        &self,
313        solution1: &MultiObjectiveSolution,
314        solution2: &MultiObjectiveSolution,
315    ) -> bool {
316        let mut at_least_one_better = false;
317
318        for (val1, val2) in solution1
319            .objective_values
320            .iter()
321            .zip(&solution2.objective_values)
322        {
323            if val1 < val2 {
324                return false; // Assuming minimization
325            }
326            if val1 > val2 {
327                at_least_one_better = true;
328            }
329        }
330
331        at_least_one_better
332    }
333
334    /// Update frontier statistics
335    fn update_frontier_statistics(&mut self) {
336        self.pareto_frontier.statistics.size = self.pareto_frontier.solutions.len();
337
338        // Calculate hypervolume (simplified)
339        self.pareto_frontier.statistics.hypervolume = self.calculate_hypervolume();
340
341        // Calculate spread
342        self.pareto_frontier.statistics.spread = self.calculate_spread();
343
344        // Update convergence metric
345        self.pareto_frontier.statistics.convergence = 0.8; // Simplified
346
347        // Update coverage
348        self.pareto_frontier.statistics.coverage = 0.9; // Simplified
349    }
350
351    /// Calculate hypervolume (simplified implementation)
352    fn calculate_hypervolume(&self) -> f64 {
353        if self.pareto_frontier.solutions.is_empty() {
354            return 0.0;
355        }
356
357        // Simple hypervolume calculation
358        let mut volume = 0.0;
359        for solution in &self.pareto_frontier.solutions {
360            let mut point_volume = 1.0;
361            for &value in &solution.objective_values {
362                point_volume *= value.max(0.0);
363            }
364            volume += point_volume;
365        }
366
367        volume
368    }
369
370    /// Calculate spread metric
371    fn calculate_spread(&self) -> f64 {
372        if self.pareto_frontier.solutions.len() < 2 {
373            return 0.0;
374        }
375
376        // Simple spread calculation based on distance between solutions
377        let mut total_distance = 0.0;
378        let num_objectives = self.pareto_frontier.solutions[0].objective_values.len();
379
380        for i in 0..num_objectives {
381            let mut values: Vec<f64> = self
382                .pareto_frontier
383                .solutions
384                .iter()
385                .map(|s| s.objective_values[i])
386                .collect();
387            values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
388
389            if let (Some(&min), Some(&max)) = (values.first(), values.last()) {
390                total_distance += max - min;
391            }
392        }
393
394        total_distance / num_objectives as f64
395    }
396
397    /// Scalarize objectives using weighted sum
398    #[must_use]
399    pub fn scalarize_weighted_sum(
400        &self,
401        solution: &MultiObjectiveSolution,
402        weights: &[f64],
403    ) -> f64 {
404        solution
405            .objective_values
406            .iter()
407            .zip(weights)
408            .map(|(value, weight)| value * weight)
409            .sum()
410    }
411
412    /// Select best solution using decision maker preferences
413    pub fn select_solution(&mut self) -> ApplicationResult<Option<String>> {
414        if self.pareto_frontier.solutions.is_empty() {
415            return Ok(None);
416        }
417
418        match self.decision_maker.strategy {
419            DecisionStrategy::Automated => {
420                // Use weighted sum with user preferences
421                let weights = &self.decision_maker.preferences.objective_weights;
422
423                let mut best_solution = None;
424                let mut best_score = f64::NEG_INFINITY;
425
426                for solution in &self.pareto_frontier.solutions {
427                    let score = self.scalarize_weighted_sum(solution, weights);
428                    if score > best_score {
429                        best_score = score;
430                        best_solution = Some(solution.id.clone());
431                    }
432                }
433
434                if let Some(ref solution_id) = best_solution {
435                    // Record decision
436                    let decision = Decision {
437                        timestamp: Instant::now(),
438                        selected_solution: solution_id.clone(),
439                        rationale: "Automated selection using weighted sum".to_string(),
440                        confidence: 0.8,
441                        user_feedback: None,
442                    };
443                    self.decision_maker.decision_history.push_back(decision);
444
445                    // Limit history size
446                    if self.decision_maker.decision_history.len() > 100 {
447                        self.decision_maker.decision_history.pop_front();
448                    }
449                }
450
451                Ok(best_solution)
452            }
453            _ => {
454                // For other strategies, just return the first solution for now
455                Ok(self.pareto_frontier.solutions.first().map(|s| s.id.clone()))
456            }
457        }
458    }
459
460    /// Get frontier statistics
461    #[must_use]
462    pub const fn get_statistics(&self) -> &FrontierStatistics {
463        &self.pareto_frontier.statistics
464    }
465
466    /// Get all solutions in Pareto frontier
467    #[must_use]
468    pub const fn get_pareto_solutions(&self) -> &Vec<MultiObjectiveSolution> {
469        &self.pareto_frontier.solutions
470    }
471
472    /// Clear Pareto frontier
473    pub fn clear_frontier(&mut self) {
474        self.pareto_frontier.solutions.clear();
475        self.update_frontier_statistics();
476
477        let update = FrontierUpdate {
478            timestamp: Instant::now(),
479            solutions_added: Vec::new(),
480            solutions_removed: Vec::new(),
481            reason: UpdateReason::QualityImprovement,
482        };
483        self.pareto_frontier.update_history.push_back(update);
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490    use crate::meta_learning::config::*;
491    use crate::meta_learning::config::{AlgorithmType, ResourceAllocation};
492
493    #[test]
494    fn test_multi_objective_optimizer_creation() {
495        let config = MultiObjectiveConfig::default();
496        let optimizer = MultiObjectiveOptimizer::new(config);
497
498        assert_eq!(optimizer.pareto_frontier.solutions.len(), 0);
499        assert_eq!(optimizer.pareto_frontier.statistics.size, 0);
500    }
501
502    #[test]
503    fn test_solution_addition() {
504        let config = MultiObjectiveConfig::default();
505        let mut optimizer = MultiObjectiveOptimizer::new(config);
506
507        let solution = MultiObjectiveSolution {
508            id: "test_solution".to_string(),
509            objective_values: vec![1.0, 2.0, 3.0],
510            decision_variables: OptimizationConfiguration {
511                algorithm: AlgorithmType::SimulatedAnnealing,
512                hyperparameters: HashMap::new(),
513                architecture: None,
514                resources: ResourceAllocation {
515                    cpu: 1.0,
516                    memory: 512,
517                    gpu: 0.0,
518                    time: Duration::from_secs(60),
519                },
520            },
521            dominance_rank: 0,
522            crowding_distance: 0.0,
523        };
524
525        let result = optimizer.add_solution(solution);
526        assert!(result.is_ok());
527        assert!(result.expect("add_solution should succeed"));
528        assert_eq!(optimizer.pareto_frontier.solutions.len(), 1);
529    }
530
531    #[test]
532    fn test_dominance_check() {
533        let config = MultiObjectiveConfig::default();
534        let optimizer = MultiObjectiveOptimizer::new(config);
535
536        let solution1 = MultiObjectiveSolution {
537            id: "solution1".to_string(),
538            objective_values: vec![1.0, 2.0],
539            decision_variables: OptimizationConfiguration {
540                algorithm: AlgorithmType::QuantumAnnealing,
541                hyperparameters: HashMap::new(),
542                architecture: None,
543                resources: ResourceAllocation {
544                    cpu: 1.0,
545                    memory: 512,
546                    gpu: 0.0,
547                    time: Duration::from_secs(60),
548                },
549            },
550            dominance_rank: 0,
551            crowding_distance: 0.0,
552        };
553
554        let solution2 = MultiObjectiveSolution {
555            id: "solution2".to_string(),
556            objective_values: vec![2.0, 1.0],
557            decision_variables: OptimizationConfiguration {
558                algorithm: AlgorithmType::TabuSearch,
559                hyperparameters: HashMap::new(),
560                architecture: None,
561                resources: ResourceAllocation {
562                    cpu: 1.0,
563                    memory: 512,
564                    gpu: 0.0,
565                    time: Duration::from_secs(60),
566                },
567            },
568            dominance_rank: 0,
569            crowding_distance: 0.0,
570        };
571
572        // Neither solution should dominate the other (trade-off)
573        assert!(!optimizer.dominates(&solution1, &solution2));
574        assert!(!optimizer.dominates(&solution2, &solution1));
575    }
576
577    #[test]
578    fn test_weighted_sum_scalarization() {
579        let config = MultiObjectiveConfig::default();
580        let optimizer = MultiObjectiveOptimizer::new(config);
581
582        let solution = MultiObjectiveSolution {
583            id: "test_solution".to_string(),
584            objective_values: vec![2.0, 3.0, 1.0],
585            decision_variables: OptimizationConfiguration {
586                algorithm: AlgorithmType::GeneticAlgorithm,
587                hyperparameters: HashMap::new(),
588                architecture: None,
589                resources: ResourceAllocation {
590                    cpu: 1.0,
591                    memory: 512,
592                    gpu: 0.0,
593                    time: Duration::from_secs(60),
594                },
595            },
596            dominance_rank: 0,
597            crowding_distance: 0.0,
598        };
599
600        let weights = vec![0.5, 0.3, 0.2];
601        let score = optimizer.scalarize_weighted_sum(&solution, &weights);
602
603        // Expected: 2.0*0.5 + 3.0*0.3 + 1.0*0.2 = 1.0 + 0.9 + 0.2 = 2.1
604        assert!((score - 2.1).abs() < 1e-10);
605    }
606
607    #[test]
608    fn test_frontier_statistics() {
609        let config = MultiObjectiveConfig::default();
610        let mut optimizer = MultiObjectiveOptimizer::new(config);
611
612        // Add a solution
613        let solution = MultiObjectiveSolution {
614            id: "test_solution".to_string(),
615            objective_values: vec![1.0, 2.0],
616            decision_variables: OptimizationConfiguration {
617                algorithm: AlgorithmType::ParticleSwarm,
618                hyperparameters: HashMap::new(),
619                architecture: None,
620                resources: ResourceAllocation {
621                    cpu: 1.0,
622                    memory: 512,
623                    gpu: 0.0,
624                    time: Duration::from_secs(60),
625                },
626            },
627            dominance_rank: 0,
628            crowding_distance: 0.0,
629        };
630
631        optimizer
632            .add_solution(solution)
633            .expect("add_solution should succeed");
634
635        let stats = optimizer.get_statistics();
636        assert_eq!(stats.size, 1);
637        assert!(stats.hypervolume > 0.0);
638    }
639
640    #[test]
641    fn test_solution_selection() {
642        let config = MultiObjectiveConfig::default();
643        let mut optimizer = MultiObjectiveOptimizer::new(config);
644
645        // Add solutions
646        let solution1 = MultiObjectiveSolution {
647            id: "solution1".to_string(),
648            objective_values: vec![1.0, 2.0],
649            decision_variables: OptimizationConfiguration {
650                algorithm: AlgorithmType::AntColony,
651                hyperparameters: HashMap::new(),
652                architecture: None,
653                resources: ResourceAllocation {
654                    cpu: 1.0,
655                    memory: 512,
656                    gpu: 0.0,
657                    time: Duration::from_secs(60),
658                },
659            },
660            dominance_rank: 0,
661            crowding_distance: 0.0,
662        };
663
664        let solution2 = MultiObjectiveSolution {
665            id: "solution2".to_string(),
666            objective_values: vec![2.0, 1.0],
667            decision_variables: OptimizationConfiguration {
668                algorithm: AlgorithmType::VariableNeighborhood,
669                hyperparameters: HashMap::new(),
670                architecture: None,
671                resources: ResourceAllocation {
672                    cpu: 1.0,
673                    memory: 512,
674                    gpu: 0.0,
675                    time: Duration::from_secs(60),
676                },
677            },
678            dominance_rank: 0,
679            crowding_distance: 0.0,
680        };
681
682        optimizer
683            .add_solution(solution1)
684            .expect("add_solution for solution1 should succeed");
685        optimizer
686            .add_solution(solution2)
687            .expect("add_solution for solution2 should succeed");
688
689        let selected = optimizer.select_solution();
690        assert!(selected.is_ok());
691        assert!(selected.expect("select_solution should succeed").is_some());
692    }
693}