quantrs2_anneal/meta_learning_optimization/
portfolio_management.rs

1//! Algorithm portfolio management for meta-learning optimization
2
3use std::collections::{HashMap, VecDeque};
4use std::time::{Duration, Instant};
5
6use scirs2_core::rand_prelude::IndexedRandom;
7use scirs2_core::random::thread_rng;
8
9use super::config::{
10    AlgorithmSelectionStrategy, DiversityCriteria, DiversityMethod, PortfolioManagementConfig,
11};
12use super::feature_extraction::{
13    AlgorithmType, OptimizationConfiguration, ProblemDomain, ProblemFeatures, ResourceAllocation,
14    ResourceUsage,
15};
16
17/// Algorithm portfolio manager
18pub struct AlgorithmPortfolio {
19    /// Available algorithms
20    pub algorithms: HashMap<String, Algorithm>,
21    /// Portfolio composition
22    pub composition: PortfolioComposition,
23    /// Selection strategy
24    pub selection_strategy: AlgorithmSelectionStrategy,
25    /// Performance history
26    pub performance_history: HashMap<String, VecDeque<PerformanceRecord>>,
27    /// Diversity analyzer
28    pub diversity_analyzer: DiversityAnalyzer,
29}
30
31impl AlgorithmPortfolio {
32    #[must_use]
33    pub fn new(config: PortfolioManagementConfig) -> Self {
34        let mut portfolio = Self {
35            algorithms: HashMap::new(),
36            composition: PortfolioComposition::new(),
37            selection_strategy: config.selection_strategy,
38            performance_history: HashMap::new(),
39            diversity_analyzer: DiversityAnalyzer::new(config.diversity_criteria),
40        };
41
42        // Initialize with default algorithms
43        portfolio.add_default_algorithms();
44        portfolio
45    }
46
47    fn add_default_algorithms(&mut self) {
48        // Add simulated annealing
49        let sa_algorithm = Algorithm {
50            id: "simulated_annealing".to_string(),
51            algorithm_type: AlgorithmType::SimulatedAnnealing,
52            default_config: OptimizationConfiguration {
53                algorithm: AlgorithmType::SimulatedAnnealing,
54                hyperparameters: [
55                    ("initial_temperature".to_string(), 10.0),
56                    ("final_temperature".to_string(), 0.1),
57                    ("cooling_rate".to_string(), 0.95),
58                ]
59                .iter()
60                .cloned()
61                .collect(),
62                architecture: None,
63                resources: ResourceAllocation {
64                    cpu: 1.0,
65                    memory: 256,
66                    gpu: 0.0,
67                    time: Duration::from_secs(300),
68                },
69            },
70            performance_stats: AlgorithmPerformanceStats::default(),
71            applicability: ApplicabilityConditions {
72                size_range: (1, 10_000),
73                suitable_domains: vec![
74                    ProblemDomain::Combinatorial,
75                    ProblemDomain::Graph,
76                    ProblemDomain::Scheduling,
77                ],
78                required_resources: ResourceRequirements {
79                    memory: 128,
80                    compute_time: Duration::from_secs(60),
81                    parameters: 1000,
82                    flops: 1_000_000,
83                },
84                performance_guarantees: vec![],
85            },
86        };
87
88        self.algorithms
89            .insert("simulated_annealing".to_string(), sa_algorithm);
90
91        // Add quantum annealing
92        let qa_algorithm = Algorithm {
93            id: "quantum_annealing".to_string(),
94            algorithm_type: AlgorithmType::QuantumAnnealing,
95            default_config: OptimizationConfiguration {
96                algorithm: AlgorithmType::QuantumAnnealing,
97                hyperparameters: [
98                    ("annealing_time".to_string(), 20.0),
99                    ("num_reads".to_string(), 1000.0),
100                    ("chain_strength".to_string(), 1.0),
101                ]
102                .iter()
103                .cloned()
104                .collect(),
105                architecture: None,
106                resources: ResourceAllocation {
107                    cpu: 0.5,
108                    memory: 512,
109                    gpu: 0.0,
110                    time: Duration::from_secs(60),
111                },
112            },
113            performance_stats: AlgorithmPerformanceStats::default(),
114            applicability: ApplicabilityConditions {
115                size_range: (1, 5000),
116                suitable_domains: vec![
117                    ProblemDomain::Combinatorial,
118                    ProblemDomain::Physics,
119                    ProblemDomain::MachineLearning,
120                ],
121                required_resources: ResourceRequirements {
122                    memory: 256,
123                    compute_time: Duration::from_secs(30),
124                    parameters: 5000,
125                    flops: 10_000_000,
126                },
127                performance_guarantees: vec![],
128            },
129        };
130
131        self.algorithms
132            .insert("quantum_annealing".to_string(), qa_algorithm);
133
134        // Add tabu search
135        let ts_algorithm = Algorithm {
136            id: "tabu_search".to_string(),
137            algorithm_type: AlgorithmType::TabuSearch,
138            default_config: OptimizationConfiguration {
139                algorithm: AlgorithmType::TabuSearch,
140                hyperparameters: [
141                    ("tabu_size".to_string(), 50.0),
142                    ("max_iterations".to_string(), 1000.0),
143                    ("aspiration_criteria".to_string(), 1.0),
144                ]
145                .iter()
146                .cloned()
147                .collect(),
148                architecture: None,
149                resources: ResourceAllocation {
150                    cpu: 1.0,
151                    memory: 512,
152                    gpu: 0.0,
153                    time: Duration::from_secs(600),
154                },
155            },
156            performance_stats: AlgorithmPerformanceStats::default(),
157            applicability: ApplicabilityConditions {
158                size_range: (10, 50_000),
159                suitable_domains: vec![
160                    ProblemDomain::Combinatorial,
161                    ProblemDomain::Scheduling,
162                    ProblemDomain::Graph,
163                ],
164                required_resources: ResourceRequirements {
165                    memory: 256,
166                    compute_time: Duration::from_secs(120),
167                    parameters: 10_000,
168                    flops: 50_000_000,
169                },
170                performance_guarantees: vec![],
171            },
172        };
173
174        self.algorithms
175            .insert("tabu_search".to_string(), ts_algorithm);
176    }
177
178    pub fn select_algorithm(
179        &mut self,
180        problem_features: &ProblemFeatures,
181    ) -> Result<String, String> {
182        match &self.selection_strategy {
183            AlgorithmSelectionStrategy::MultiArmedBandit => {
184                self.multi_armed_bandit_selection(problem_features)
185            }
186            AlgorithmSelectionStrategy::UpperConfidenceBound => {
187                self.ucb_selection(problem_features)
188            }
189            AlgorithmSelectionStrategy::ThompsonSampling => {
190                self.thompson_sampling_selection(problem_features)
191            }
192            AlgorithmSelectionStrategy::EpsilonGreedy(epsilon) => {
193                self.epsilon_greedy_selection(problem_features, *epsilon)
194            }
195            AlgorithmSelectionStrategy::CollaborativeFiltering => {
196                self.collaborative_filtering_selection(problem_features)
197            }
198            AlgorithmSelectionStrategy::MetaLearningBased => {
199                self.meta_learning_selection(problem_features)
200            }
201        }
202    }
203
204    fn multi_armed_bandit_selection(
205        &self,
206        problem_features: &ProblemFeatures,
207    ) -> Result<String, String> {
208        // Simplified multi-armed bandit using average performance
209        let applicable_algorithms = self.get_applicable_algorithms(problem_features);
210
211        if applicable_algorithms.is_empty() {
212            return Err("No applicable algorithms found".to_string());
213        }
214
215        let best_algorithm = applicable_algorithms
216            .iter()
217            .max_by(|a, b| {
218                let perf_a = self
219                    .algorithms
220                    .get(*a)
221                    .map_or(0.0, |alg| alg.performance_stats.mean_performance);
222                let perf_b = self
223                    .algorithms
224                    .get(*b)
225                    .map_or(0.0, |alg| alg.performance_stats.mean_performance);
226                perf_a
227                    .partial_cmp(&perf_b)
228                    .unwrap_or(std::cmp::Ordering::Equal)
229            })
230            .ok_or("Failed to select algorithm")?;
231
232        Ok(best_algorithm.clone())
233    }
234
235    fn ucb_selection(&self, problem_features: &ProblemFeatures) -> Result<String, String> {
236        let applicable_algorithms = self.get_applicable_algorithms(problem_features);
237
238        if applicable_algorithms.is_empty() {
239            return Err("No applicable algorithms found".to_string());
240        }
241
242        // Simplified UCB calculation
243        let total_trials: f64 = self
244            .performance_history
245            .values()
246            .map(|history| history.len() as f64)
247            .sum();
248
249        let best_algorithm = applicable_algorithms
250            .iter()
251            .max_by(|a, b| {
252                let history_a = self
253                    .performance_history
254                    .get(*a)
255                    .map_or(0, std::collections::VecDeque::len);
256                let history_b = self
257                    .performance_history
258                    .get(*b)
259                    .map_or(0, std::collections::VecDeque::len);
260
261                let mean_a = self
262                    .algorithms
263                    .get(*a)
264                    .map_or(0.0, |alg| alg.performance_stats.mean_performance);
265                let mean_b = self
266                    .algorithms
267                    .get(*b)
268                    .map_or(0.0, |alg| alg.performance_stats.mean_performance);
269
270                let confidence_a = if history_a > 0 {
271                    (2.0 * total_trials.ln() / history_a as f64).sqrt()
272                } else {
273                    f64::INFINITY
274                };
275
276                let confidence_b = if history_b > 0 {
277                    (2.0 * total_trials.ln() / history_b as f64).sqrt()
278                } else {
279                    f64::INFINITY
280                };
281
282                let ucb_a = mean_a + confidence_a;
283                let ucb_b = mean_b + confidence_b;
284
285                ucb_a
286                    .partial_cmp(&ucb_b)
287                    .unwrap_or(std::cmp::Ordering::Equal)
288            })
289            .ok_or("Failed to select algorithm")?;
290
291        Ok(best_algorithm.clone())
292    }
293
294    fn thompson_sampling_selection(
295        &self,
296        problem_features: &ProblemFeatures,
297    ) -> Result<String, String> {
298        // Simplified Thompson sampling - just return the algorithm with highest variance
299        let applicable_algorithms = self.get_applicable_algorithms(problem_features);
300
301        if applicable_algorithms.is_empty() {
302            return Err("No applicable algorithms found".to_string());
303        }
304
305        let best_algorithm = applicable_algorithms
306            .iter()
307            .max_by(|a, b| {
308                let var_a = self
309                    .algorithms
310                    .get(*a)
311                    .map_or(0.0, |alg| alg.performance_stats.performance_variance);
312                let var_b = self
313                    .algorithms
314                    .get(*b)
315                    .map_or(0.0, |alg| alg.performance_stats.performance_variance);
316                var_a
317                    .partial_cmp(&var_b)
318                    .unwrap_or(std::cmp::Ordering::Equal)
319            })
320            .ok_or("Failed to select algorithm")?;
321
322        Ok(best_algorithm.clone())
323    }
324
325    fn epsilon_greedy_selection(
326        &self,
327        problem_features: &ProblemFeatures,
328        epsilon: f64,
329    ) -> Result<String, String> {
330        use scirs2_core::random::prelude::*;
331        let mut rng = thread_rng();
332
333        let applicable_algorithms = self.get_applicable_algorithms(problem_features);
334
335        if applicable_algorithms.is_empty() {
336            return Err("No applicable algorithms found".to_string());
337        }
338
339        if rng.gen_bool(epsilon) {
340            // Explore: random selection
341            let algorithm = applicable_algorithms
342                .choose(&mut rng)
343                .ok_or("Failed to randomly select algorithm")?;
344            Ok(algorithm.clone())
345        } else {
346            // Exploit: best known algorithm
347            self.multi_armed_bandit_selection(problem_features)
348        }
349    }
350
351    fn collaborative_filtering_selection(
352        &self,
353        problem_features: &ProblemFeatures,
354    ) -> Result<String, String> {
355        // Simplified collaborative filtering based on problem similarity
356        self.multi_armed_bandit_selection(problem_features)
357    }
358
359    fn meta_learning_selection(
360        &self,
361        problem_features: &ProblemFeatures,
362    ) -> Result<String, String> {
363        // Simplified meta-learning selection
364        self.multi_armed_bandit_selection(problem_features)
365    }
366
367    fn get_applicable_algorithms(&self, problem_features: &ProblemFeatures) -> Vec<String> {
368        self.algorithms
369            .iter()
370            .filter(|(_, algorithm)| {
371                // Check size range
372                problem_features.size >= algorithm.applicability.size_range.0
373                    && problem_features.size <= algorithm.applicability.size_range.1
374            })
375            .map(|(id, _)| id.clone())
376            .collect()
377    }
378
379    pub fn record_performance(&mut self, algorithm_id: &str, record: PerformanceRecord) {
380        self.performance_history
381            .entry(algorithm_id.to_string())
382            .or_insert_with(VecDeque::new)
383            .push_back(record);
384
385        // Update algorithm statistics - extract algorithm to avoid double mutable borrow
386        let algorithm_id_string = algorithm_id.to_string();
387        if self.algorithms.contains_key(algorithm_id) {
388            // Get the performance history first
389            let history = self.performance_history.get(&algorithm_id_string).cloned();
390            if let Some(algorithm) = self.algorithms.get_mut(algorithm_id) {
391                if let Some(history) = history {
392                    if !history.is_empty() {
393                        // Update mean performance
394                        let total_performance: f64 = history.iter().map(|r| r.performance).sum();
395                        algorithm.performance_stats.mean_performance =
396                            total_performance / history.len() as f64;
397
398                        // Update variance
399                        let variance: f64 = history
400                            .iter()
401                            .map(|r| {
402                                (r.performance - algorithm.performance_stats.mean_performance)
403                                    .powi(2)
404                            })
405                            .sum::<f64>()
406                            / history.len() as f64;
407                        algorithm.performance_stats.performance_variance = variance;
408
409                        // Update success rate (consider performance > 0.5 as success)
410                        let successful_runs =
411                            history.iter().filter(|r| r.performance > 0.5).count();
412                        algorithm.performance_stats.success_rate =
413                            successful_runs as f64 / history.len() as f64;
414                    }
415                }
416            }
417        }
418
419        // Limit history size
420        if let Some(history) = self.performance_history.get_mut(algorithm_id) {
421            while history.len() > 1000 {
422                history.pop_front();
423            }
424        }
425    }
426
427    fn update_algorithm_stats(&self, algorithm: &mut Algorithm, _record: &PerformanceRecord) {
428        let Some(history) = self.performance_history.get(&algorithm.id) else {
429            return;
430        };
431
432        if !history.is_empty() {
433            // Update mean performance
434            let total_performance: f64 = history.iter().map(|r| r.performance).sum();
435            algorithm.performance_stats.mean_performance = total_performance / history.len() as f64;
436
437            // Update variance
438            let variance: f64 = history
439                .iter()
440                .map(|r| (r.performance - algorithm.performance_stats.mean_performance).powi(2))
441                .sum::<f64>()
442                / history.len() as f64;
443            algorithm.performance_stats.performance_variance = variance;
444
445            // Update success rate (simplified: performance > 0.5)
446            let successes = history.iter().filter(|r| r.performance > 0.5).count();
447            algorithm.performance_stats.success_rate = successes as f64 / history.len() as f64;
448        }
449    }
450
451    #[must_use]
452    pub const fn get_portfolio_diversity(&self) -> f64 {
453        self.diversity_analyzer.current_diversity
454    }
455
456    pub fn update_composition(&mut self) {
457        // Simple composition update based on recent performance
458        let mut new_weights = HashMap::new();
459        let mut total_performance = 0.0;
460
461        for (algorithm_id, algorithm) in &self.algorithms {
462            let weight = algorithm.performance_stats.mean_performance.max(0.1);
463            new_weights.insert(algorithm_id.clone(), weight);
464            total_performance += weight;
465        }
466
467        // Normalize weights
468        if total_performance > 0.0 {
469            for (_, weight) in &mut new_weights {
470                *weight /= total_performance;
471            }
472        }
473
474        self.composition.weights = new_weights;
475        self.composition.last_update = Instant::now();
476
477        // Update selection probabilities (same as weights for simplicity)
478        self.composition.selection_probabilities = self.composition.weights.clone();
479    }
480}
481
482/// Algorithm representation
483#[derive(Debug)]
484pub struct Algorithm {
485    /// Algorithm identifier
486    pub id: String,
487    /// Algorithm type
488    pub algorithm_type: AlgorithmType,
489    /// Default configuration
490    pub default_config: OptimizationConfiguration,
491    /// Performance statistics
492    pub performance_stats: AlgorithmPerformanceStats,
493    /// Applicability conditions
494    pub applicability: ApplicabilityConditions,
495}
496
497/// Portfolio composition
498#[derive(Debug, Clone)]
499pub struct PortfolioComposition {
500    /// Algorithm weights
501    pub weights: HashMap<String, f64>,
502    /// Selection probabilities
503    pub selection_probabilities: HashMap<String, f64>,
504    /// Last update time
505    pub last_update: Instant,
506    /// Composition quality
507    pub quality_score: f64,
508}
509
510impl PortfolioComposition {
511    #[must_use]
512    pub fn new() -> Self {
513        Self {
514            weights: HashMap::new(),
515            selection_probabilities: HashMap::new(),
516            last_update: Instant::now(),
517            quality_score: 0.0,
518        }
519    }
520}
521
522/// Performance record
523#[derive(Debug, Clone)]
524pub struct PerformanceRecord {
525    /// Timestamp
526    pub timestamp: Instant,
527    /// Problem characteristics
528    pub problem_features: ProblemFeatures,
529    /// Performance achieved
530    pub performance: f64,
531    /// Resource usage
532    pub resource_usage: ResourceUsage,
533    /// Context information
534    pub context: HashMap<String, String>,
535}
536
537/// Algorithm performance statistics
538#[derive(Debug, Clone)]
539pub struct AlgorithmPerformanceStats {
540    /// Mean performance
541    pub mean_performance: f64,
542    /// Performance variance
543    pub performance_variance: f64,
544    /// Success rate
545    pub success_rate: f64,
546    /// Average runtime
547    pub avg_runtime: Duration,
548    /// Scalability factor
549    pub scalability_factor: f64,
550}
551
552impl Default for AlgorithmPerformanceStats {
553    fn default() -> Self {
554        Self {
555            mean_performance: 0.5,
556            performance_variance: 0.1,
557            success_rate: 0.5,
558            avg_runtime: Duration::from_secs(60),
559            scalability_factor: 1.0,
560        }
561    }
562}
563
564/// Applicability conditions
565#[derive(Debug, Clone)]
566pub struct ApplicabilityConditions {
567    /// Problem size range
568    pub size_range: (usize, usize),
569    /// Suitable domains
570    pub suitable_domains: Vec<ProblemDomain>,
571    /// Required resources
572    pub required_resources: ResourceRequirements,
573    /// Performance guarantees
574    pub performance_guarantees: Vec<PerformanceGuarantee>,
575}
576
577/// Performance guarantee
578#[derive(Debug, Clone)]
579pub struct PerformanceGuarantee {
580    /// Guarantee type
581    pub guarantee_type: GuaranteeType,
582    /// Confidence level
583    pub confidence: f64,
584    /// Conditions
585    pub conditions: Vec<String>,
586}
587
588/// Types of performance guarantees
589#[derive(Debug, Clone, PartialEq)]
590pub enum GuaranteeType {
591    /// Minimum performance level
592    MinimumPerformance(f64),
593    /// Maximum runtime
594    MaximumRuntime(Duration),
595    /// Resource bounds
596    ResourceBounds(ResourceRequirements),
597    /// Quality bounds
598    QualityBounds(f64, f64),
599}
600
601/// Diversity analyzer
602#[derive(Debug)]
603pub struct DiversityAnalyzer {
604    /// Diversity metrics
605    pub metrics: Vec<DiversityMetric>,
606    /// Analysis methods
607    pub methods: Vec<DiversityMethod>,
608    /// Current diversity score
609    pub current_diversity: f64,
610    /// Target diversity
611    pub target_diversity: f64,
612}
613
614impl DiversityAnalyzer {
615    #[must_use]
616    pub fn new(criteria: DiversityCriteria) -> Self {
617        Self {
618            metrics: vec![
619                DiversityMetric::AlgorithmDiversity,
620                DiversityMetric::PerformanceDiversity,
621            ],
622            methods: vec![criteria.diversity_method],
623            current_diversity: 0.5,
624            target_diversity: criteria.min_algorithmic_diversity,
625        }
626    }
627
628    pub fn analyze_diversity(&mut self, portfolio: &AlgorithmPortfolio) -> f64 {
629        // Simplified diversity calculation
630        let num_algorithms = portfolio.algorithms.len() as f64;
631        let max_diversity = (num_algorithms * (num_algorithms - 1.0) / 2.0).max(1.0);
632
633        // Calculate algorithmic diversity
634        let algorithmic_diversity = if num_algorithms > 1.0 {
635            1.0 / num_algorithms
636        } else {
637            0.0
638        };
639
640        // Calculate performance diversity
641        let performances: Vec<f64> = portfolio
642            .algorithms
643            .values()
644            .map(|alg| alg.performance_stats.mean_performance)
645            .collect();
646
647        let performance_diversity = if performances.len() > 1 {
648            let mean_perf = performances.iter().sum::<f64>() / performances.len() as f64;
649            let variance = performances
650                .iter()
651                .map(|p| (p - mean_perf).powi(2))
652                .sum::<f64>()
653                / performances.len() as f64;
654            variance.sqrt()
655        } else {
656            0.0
657        };
658
659        self.current_diversity = f64::midpoint(algorithmic_diversity, performance_diversity);
660        self.current_diversity
661    }
662}
663
664/// Diversity metrics
665#[derive(Debug, Clone, PartialEq, Eq)]
666pub enum DiversityMetric {
667    /// Algorithm diversity
668    AlgorithmDiversity,
669    /// Performance diversity
670    PerformanceDiversity,
671    /// Feature diversity
672    FeatureDiversity,
673    /// Error diversity
674    ErrorDiversity,
675    /// Prediction diversity
676    PredictionDiversity,
677}
678
679use super::neural_architecture_search::ResourceRequirements;
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684    use crate::meta_learning_optimization::feature_extraction::{
685        GraphFeatures, SpectralFeatures, StatisticalFeatures,
686    };
687
688    #[test]
689    fn test_portfolio_creation() {
690        let config = PortfolioManagementConfig::default();
691        let portfolio = AlgorithmPortfolio::new(config);
692        assert!(!portfolio.algorithms.is_empty());
693    }
694
695    #[test]
696    fn test_algorithm_selection() {
697        let config = PortfolioManagementConfig::default();
698        let mut portfolio = AlgorithmPortfolio::new(config);
699
700        let features = ProblemFeatures {
701            size: 100,
702            density: 0.5,
703            graph_features: GraphFeatures::default(),
704            statistical_features: StatisticalFeatures::default(),
705            spectral_features: SpectralFeatures::default(),
706            domain_features: HashMap::new(),
707        };
708
709        let result = portfolio.select_algorithm(&features);
710        assert!(result.is_ok());
711    }
712
713    #[test]
714    fn test_diversity_analyzer() {
715        let criteria = DiversityCriteria::default();
716        let analyzer = DiversityAnalyzer::new(criteria);
717        assert_eq!(analyzer.target_diversity, 0.2);
718    }
719}