Skip to main content

quantrs2_anneal/
advanced_meta_optimizer.rs

1//! Advanced Meta-Learning Optimization System
2//!
3//! This module implements a sophisticated meta-learning system that learns from
4//! optimization history to improve future optimization runs. It includes:
5//! - Optimization history analysis and pattern recognition
6//! - Transfer learning between similar problems
7//! - Adaptive strategy selection based on problem characteristics
8//! - Performance prediction using learned models
9
10use crate::applications::{ApplicationError, ApplicationResult};
11use crate::ising::IsingModel;
12use scirs2_core::ndarray::{Array1, Array2};
13use scirs2_core::random::prelude::*;
14use scirs2_core::ChaCha8Rng;
15use scirs2_core::Complex64;
16use scirs2_core::RngExt;
17use serde::{Deserialize, Serialize};
18use std::collections::{HashMap, VecDeque};
19use std::time::{Duration, Instant};
20
21// Re-export for convenience
22pub use crate::ising::Coupling;
23
24/// Problem features extracted for meta-learning
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ProblemFeatures {
27    /// Number of variables
28    pub num_variables: usize,
29    /// Problem density (ratio of non-zero couplings)
30    pub density: f64,
31    /// Coupling strength statistics
32    pub coupling_mean: f64,
33    pub coupling_std: f64,
34    pub coupling_max: f64,
35    /// Bias statistics
36    pub bias_mean: f64,
37    pub bias_std: f64,
38    /// Graph properties
39    pub average_degree: f64,
40    pub max_degree: usize,
41    pub clustering_coefficient: f64,
42    /// Energy landscape properties
43    pub estimated_barriers: f64,
44    pub frustration_index: f64,
45    /// Problem symmetry
46    pub symmetry_score: f64,
47}
48
49/// Optimization run record for learning
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct OptimizationRecord {
52    /// Problem features
53    pub features: ProblemFeatures,
54    /// Strategy used
55    pub strategy: OptimizationStrategy,
56    /// Parameters used
57    pub parameters: HashMap<String, f64>,
58    /// Performance metrics
59    pub best_energy: f64,
60    pub convergence_time: Duration,
61    pub iterations_to_converge: usize,
62    pub success_rate: f64,
63    /// Resource usage
64    pub cpu_time: Duration,
65    pub memory_peak_mb: f64,
66    /// Timestamp (not serialized, defaults to now)
67    #[serde(skip, default = "Instant::now")]
68    pub timestamp: Instant,
69}
70
71/// Available optimization strategies
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
73pub enum OptimizationStrategy {
74    ClassicalAnnealing,
75    QuantumAnnealing,
76    PopulationAnnealing,
77    CoherentIsingMachine,
78    QuantumWalk,
79    HybridQCML,
80    AdaptiveSchedule,
81    ReversedAnnealing,
82}
83
84/// Performance prediction model
85#[derive(Debug, Clone)]
86pub struct PerformancePredictor {
87    /// Feature weights learned from history
88    feature_weights: Array1<f64>,
89    /// Strategy-specific adjustments
90    strategy_adjustments: HashMap<OptimizationStrategy, f64>,
91    /// Prediction confidence
92    confidence: f64,
93}
94
95impl PerformancePredictor {
96    /// Create a new predictor
97    #[must_use]
98    pub fn new(num_features: usize) -> Self {
99        Self {
100            feature_weights: Array1::zeros(num_features),
101            strategy_adjustments: HashMap::new(),
102            confidence: 0.0,
103        }
104    }
105
106    /// Predict performance for given features and strategy
107    #[must_use]
108    pub fn predict(
109        &self,
110        features: &ProblemFeatures,
111        strategy: OptimizationStrategy,
112    ) -> PredictedPerformance {
113        let feature_vec = self.features_to_vector(features);
114        let base_score = feature_vec.dot(&self.feature_weights);
115
116        let strategy_adj = self
117            .strategy_adjustments
118            .get(&strategy)
119            .copied()
120            .unwrap_or(0.0);
121
122        let predicted_quality = (base_score + strategy_adj).tanh();
123        let predicted_time = self.estimate_time(features, strategy);
124
125        PredictedPerformance {
126            strategy,
127            quality_score: predicted_quality,
128            estimated_time: predicted_time,
129            confidence: self.confidence,
130        }
131    }
132
133    /// Convert features to vector for prediction
134    fn features_to_vector(&self, features: &ProblemFeatures) -> Array1<f64> {
135        Array1::from_vec(vec![
136            features.num_variables as f64,
137            features.density,
138            features.coupling_mean,
139            features.coupling_std,
140            features.average_degree,
141            features.clustering_coefficient,
142            features.frustration_index,
143            features.symmetry_score,
144        ])
145    }
146
147    /// Estimate execution time based on problem size and strategy
148    fn estimate_time(
149        &self,
150        features: &ProblemFeatures,
151        strategy: OptimizationStrategy,
152    ) -> Duration {
153        let base_complexity = match strategy {
154            OptimizationStrategy::ClassicalAnnealing => features.num_variables as f64,
155            OptimizationStrategy::QuantumAnnealing => (features.num_variables as f64).powf(1.5),
156            OptimizationStrategy::PopulationAnnealing => {
157                features.num_variables as f64 * features.density
158            }
159            OptimizationStrategy::CoherentIsingMachine => (features.num_variables as f64).powi(2),
160            OptimizationStrategy::QuantumWalk => {
161                features.num_variables as f64 * features.average_degree
162            }
163            OptimizationStrategy::HybridQCML => features.num_variables as f64 * 1.5,
164            OptimizationStrategy::AdaptiveSchedule => features.num_variables as f64 * 0.8,
165            OptimizationStrategy::ReversedAnnealing => features.num_variables as f64 * 0.6,
166        };
167
168        // Scale by problem difficulty
169        let difficulty_factor = 1.0 + features.frustration_index + (1.0 - features.symmetry_score);
170        let estimated_ms = base_complexity * difficulty_factor * 10.0;
171
172        Duration::from_millis(estimated_ms as u64)
173    }
174
175    /// Update predictor with new observation
176    pub fn update(&mut self, record: &OptimizationRecord, learning_rate: f64) {
177        let feature_vec = self.features_to_vector(&record.features);
178
179        // Simple gradient update (could be replaced with more sophisticated learning)
180        let predicted = self.predict(&record.features, record.strategy);
181        let error = record.success_rate - predicted.quality_score;
182
183        for i in 0..self.feature_weights.len() {
184            self.feature_weights[i] += learning_rate * error * feature_vec[i];
185        }
186
187        // Update strategy adjustment
188        let current_adj = self
189            .strategy_adjustments
190            .entry(record.strategy)
191            .or_insert(0.0);
192        *current_adj += learning_rate * error * 0.1;
193
194        // Update confidence based on error
195        self.confidence = 0.9f64.mul_add(self.confidence, 0.1 * (1.0 - error.abs()));
196    }
197}
198
199/// Predicted performance metrics
200#[derive(Debug, Clone)]
201pub struct PredictedPerformance {
202    pub strategy: OptimizationStrategy,
203    pub quality_score: f64,
204    pub estimated_time: Duration,
205    pub confidence: f64,
206}
207
208/// Transfer learning engine for cross-problem knowledge transfer
209#[derive(Debug, Clone)]
210pub struct TransferLearningEngine {
211    /// Source domain knowledge
212    source_records: Vec<OptimizationRecord>,
213    /// Similarity metrics between problems
214    similarity_cache: HashMap<(usize, usize), f64>,
215    /// Transfer weights
216    transfer_weights: Vec<f64>,
217}
218
219impl TransferLearningEngine {
220    /// Create new transfer learning engine
221    #[must_use]
222    pub fn new() -> Self {
223        Self {
224            source_records: Vec::new(),
225            similarity_cache: HashMap::new(),
226            transfer_weights: Vec::new(),
227        }
228    }
229
230    /// Add source domain knowledge
231    pub fn add_source_knowledge(&mut self, records: Vec<OptimizationRecord>) {
232        self.source_records.extend(records);
233        self.similarity_cache.clear(); // Invalidate cache
234    }
235
236    /// Compute similarity between two problems
237    #[must_use]
238    pub fn compute_similarity(
239        &self,
240        features1: &ProblemFeatures,
241        features2: &ProblemFeatures,
242    ) -> f64 {
243        // Euclidean distance in normalized feature space
244        let diff_size = ((features1.num_variables as f64 - features2.num_variables as f64)
245            / features1.num_variables.max(features2.num_variables) as f64)
246            .powi(2);
247        let diff_density = (features1.density - features2.density).powi(2);
248        let diff_frustration = (features1.frustration_index - features2.frustration_index).powi(2);
249        let diff_symmetry = (features1.symmetry_score - features2.symmetry_score).powi(2);
250
251        let distance = (diff_size + diff_density + diff_frustration + diff_symmetry).sqrt();
252
253        // Convert distance to similarity (Gaussian kernel)
254        let bandwidth: f64 = 0.5;
255        (-distance.powi(2) / (2.0 * bandwidth.powi(2))).exp()
256    }
257
258    /// Transfer knowledge to target problem
259    pub fn transfer_knowledge(
260        &self,
261        target_features: &ProblemFeatures,
262    ) -> Vec<(OptimizationStrategy, f64)> {
263        let mut strategy_scores: HashMap<OptimizationStrategy, Vec<f64>> = HashMap::new();
264
265        for record in &self.source_records {
266            let similarity = self.compute_similarity(target_features, &record.features);
267
268            // Only transfer from sufficiently similar problems
269            if similarity > 0.3 {
270                let weighted_score = record.success_rate * similarity;
271                strategy_scores
272                    .entry(record.strategy)
273                    .or_insert_with(Vec::new)
274                    .push(weighted_score);
275            }
276        }
277
278        // Aggregate scores for each strategy
279        let mut recommendations: Vec<(OptimizationStrategy, f64)> = strategy_scores
280            .into_iter()
281            .map(|(strategy, scores)| {
282                let avg_score = scores.iter().sum::<f64>() / scores.len() as f64;
283                (strategy, avg_score)
284            })
285            .collect();
286
287        // Sort by score descending
288        recommendations.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
289
290        recommendations
291    }
292}
293
294impl Default for TransferLearningEngine {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300/// Adaptive strategy selector
301#[derive(Debug)]
302pub struct AdaptiveStrategySelector {
303    /// Performance predictor
304    predictor: PerformancePredictor,
305    /// Transfer learning engine
306    transfer_engine: TransferLearningEngine,
307    /// Strategy exploration rate
308    exploration_rate: f64,
309    /// Random number generator
310    rng: ChaCha8Rng,
311}
312
313impl AdaptiveStrategySelector {
314    /// Create new adaptive selector
315    #[must_use]
316    pub fn new(seed: u64) -> Self {
317        Self {
318            predictor: PerformancePredictor::new(8),
319            transfer_engine: TransferLearningEngine::new(),
320            exploration_rate: 0.1,
321            rng: ChaCha8Rng::seed_from_u64(seed),
322        }
323    }
324
325    /// Select best strategy for given problem
326    pub fn select_strategy(
327        &mut self,
328        features: &ProblemFeatures,
329        available_strategies: &[OptimizationStrategy],
330    ) -> OptimizationStrategy {
331        // Exploration: randomly select strategy
332        if self.rng.random::<f64>() < self.exploration_rate {
333            return *available_strategies
334                .get(self.rng.random_range(0..available_strategies.len()))
335                .expect("random index should be within bounds");
336        }
337
338        // Exploitation: use learned knowledge
339        let mut best_strategy = available_strategies[0];
340        let mut best_score = f64::NEG_INFINITY;
341
342        for &strategy in available_strategies {
343            let prediction = self.predictor.predict(features, strategy);
344            let transfer_bonus = self.get_transfer_bonus(features, strategy);
345            let total_score = 0.3f64.mul_add(transfer_bonus, prediction.quality_score);
346
347            if total_score > best_score {
348                best_score = total_score;
349                best_strategy = strategy;
350            }
351        }
352
353        best_strategy
354    }
355
356    /// Get transfer learning bonus for strategy
357    fn get_transfer_bonus(
358        &self,
359        features: &ProblemFeatures,
360        strategy: OptimizationStrategy,
361    ) -> f64 {
362        let recommendations = self.transfer_engine.transfer_knowledge(features);
363
364        recommendations
365            .iter()
366            .find(|(s, _)| *s == strategy)
367            .map_or(0.0, |(_, score)| *score)
368    }
369
370    /// Update selector with new observation
371    pub fn update(&mut self, record: OptimizationRecord) {
372        self.predictor.update(&record, 0.01);
373        self.transfer_engine.add_source_knowledge(vec![record]);
374
375        // Decay exploration rate
376        self.exploration_rate *= 0.999;
377    }
378
379    /// Get performance prediction for strategy
380    #[must_use]
381    pub fn predict_performance(
382        &self,
383        features: &ProblemFeatures,
384        strategy: OptimizationStrategy,
385    ) -> PredictedPerformance {
386        self.predictor.predict(features, strategy)
387    }
388}
389
390/// Meta-learning optimizer that learns from optimization history
391#[derive(Debug)]
392pub struct MetaLearningOptimizer {
393    /// Optimization history
394    history: VecDeque<OptimizationRecord>,
395    /// Maximum history size
396    max_history: usize,
397    /// Strategy selector
398    selector: AdaptiveStrategySelector,
399    /// Statistics
400    pub total_optimizations: usize,
401    pub average_success_rate: f64,
402}
403
404impl MetaLearningOptimizer {
405    /// Create new meta-learning optimizer
406    #[must_use]
407    pub fn new(max_history: usize, seed: u64) -> Self {
408        Self {
409            history: VecDeque::new(),
410            max_history,
411            selector: AdaptiveStrategySelector::new(seed),
412            total_optimizations: 0,
413            average_success_rate: 0.0,
414        }
415    }
416
417    /// Extract features from Ising model
418    pub fn extract_features(&self, model: &IsingModel) -> ProblemFeatures {
419        let num_variables = model.num_qubits;
420
421        // Calculate coupling statistics using public API
422        let couplings = model.couplings();
423        let mut coupling_values = Vec::new();
424        let mut degrees = vec![0; num_variables];
425
426        for coupling in &couplings {
427            coupling_values.push(coupling.strength.abs());
428            degrees[coupling.i] += 1;
429            degrees[coupling.j] += 1;
430        }
431
432        let density = couplings.len() as f64 / (num_variables * (num_variables - 1) / 2) as f64;
433
434        let coupling_mean = if coupling_values.is_empty() {
435            0.0
436        } else {
437            coupling_values.iter().sum::<f64>() / coupling_values.len() as f64
438        };
439
440        let coupling_std = if coupling_values.len() > 1 {
441            let variance = coupling_values
442                .iter()
443                .map(|v| (v - coupling_mean).powi(2))
444                .sum::<f64>()
445                / coupling_values.len() as f64;
446            variance.sqrt()
447        } else {
448            0.0
449        };
450
451        let coupling_max = coupling_values.iter().copied().fold(0.0, f64::max);
452
453        // Bias statistics using public API
454        let biases = model.biases();
455        let bias_values: Vec<f64> = biases.iter().map(|(_, b)| *b).collect();
456        let bias_mean = if bias_values.is_empty() {
457            0.0
458        } else {
459            bias_values.iter().sum::<f64>() / bias_values.len() as f64
460        };
461
462        let bias_std = if bias_values.len() > 1 {
463            let variance = bias_values
464                .iter()
465                .map(|v| (v - bias_mean).powi(2))
466                .sum::<f64>()
467                / bias_values.len() as f64;
468            variance.sqrt()
469        } else {
470            0.0
471        };
472
473        // Graph properties
474        let average_degree = if degrees.is_empty() {
475            0.0
476        } else {
477            degrees.iter().sum::<usize>() as f64 / degrees.len() as f64
478        };
479
480        let max_degree = degrees.iter().copied().max().unwrap_or(0);
481
482        // Simple clustering coefficient estimate
483        let clustering_coefficient = self.estimate_clustering(model);
484
485        // Energy landscape properties (simplified)
486        let estimated_barriers = coupling_std / (1.0 + density);
487        let frustration_index = self.estimate_frustration(model);
488
489        // Symmetry (simplified - based on bias uniformity)
490        let symmetry_score = 1.0 - (bias_std / (1.0 + bias_mean.abs()));
491
492        ProblemFeatures {
493            num_variables,
494            density,
495            coupling_mean,
496            coupling_std,
497            coupling_max,
498            bias_mean,
499            bias_std,
500            average_degree,
501            max_degree,
502            clustering_coefficient,
503            estimated_barriers,
504            frustration_index,
505            symmetry_score: symmetry_score.clamp(0.0, 1.0),
506        }
507    }
508
509    /// Estimate clustering coefficient
510    fn estimate_clustering(&self, model: &IsingModel) -> f64 {
511        // Simplified clustering coefficient calculation using public API
512        let couplings = model.couplings();
513
514        // Build adjacency map
515        let mut adj: HashMap<usize, Vec<usize>> = HashMap::new();
516        for coupling in &couplings {
517            adj.entry(coupling.i)
518                .or_insert_with(Vec::new)
519                .push(coupling.j);
520            adj.entry(coupling.j)
521                .or_insert_with(Vec::new)
522                .push(coupling.i);
523        }
524
525        let mut triangles = 0;
526        let mut triples = 0;
527
528        for i in 0..model.num_qubits {
529            if let Some(neighbors) = adj.get(&i) {
530                for k in 0..neighbors.len() {
531                    for l in (k + 1)..neighbors.len() {
532                        triples += 1;
533                        let j1 = neighbors[k];
534                        let j2 = neighbors[l];
535
536                        // Check if j1 and j2 are connected
537                        if let Some(j1_neighbors) = adj.get(&j1) {
538                            if j1_neighbors.contains(&j2) {
539                                triangles += 1;
540                            }
541                        }
542                    }
543                }
544            }
545        }
546
547        if triples > 0 {
548            f64::from(triangles) / f64::from(triples)
549        } else {
550            0.0
551        }
552    }
553
554    /// Estimate frustration index
555    fn estimate_frustration(&self, model: &IsingModel) -> f64 {
556        // Count frustrated interactions (antiferromagnetic couplings) using public API
557        let couplings = model.couplings();
558        let mut frustrated = 0;
559        let total = couplings.len();
560
561        for coupling in &couplings {
562            if coupling.strength > 0.0 {
563                // Antiferromagnetic
564                frustrated += 1;
565            }
566        }
567
568        if total > 0 {
569            f64::from(frustrated) / total as f64
570        } else {
571            0.0
572        }
573    }
574
575    /// Select best strategy for problem
576    pub fn select_strategy(&mut self, model: &IsingModel) -> OptimizationStrategy {
577        let features = self.extract_features(model);
578        let available = vec![
579            OptimizationStrategy::ClassicalAnnealing,
580            OptimizationStrategy::QuantumAnnealing,
581            OptimizationStrategy::PopulationAnnealing,
582            OptimizationStrategy::AdaptiveSchedule,
583        ];
584
585        self.selector.select_strategy(&features, &available)
586    }
587
588    /// Record optimization result
589    pub fn record_optimization(&mut self, record: OptimizationRecord) {
590        self.total_optimizations += 1;
591
592        // Update running average
593        self.average_success_rate = self
594            .average_success_rate
595            .mul_add((self.total_optimizations - 1) as f64, record.success_rate)
596            / self.total_optimizations as f64;
597
598        // Update selector
599        self.selector.update(record.clone());
600
601        // Add to history
602        self.history.push_back(record);
603
604        // Limit history size
605        if self.history.len() > self.max_history {
606            self.history.pop_front();
607        }
608    }
609
610    /// Get recommended strategies for problem
611    pub fn recommend_strategies(
612        &mut self,
613        model: &IsingModel,
614        top_k: usize,
615    ) -> Vec<(OptimizationStrategy, PredictedPerformance)> {
616        let features = self.extract_features(model);
617        let strategies = vec![
618            OptimizationStrategy::ClassicalAnnealing,
619            OptimizationStrategy::QuantumAnnealing,
620            OptimizationStrategy::PopulationAnnealing,
621            OptimizationStrategy::CoherentIsingMachine,
622            OptimizationStrategy::QuantumWalk,
623            OptimizationStrategy::HybridQCML,
624            OptimizationStrategy::AdaptiveSchedule,
625            OptimizationStrategy::ReversedAnnealing,
626        ];
627
628        let mut recommendations: Vec<_> = strategies
629            .iter()
630            .map(|&strategy| {
631                let prediction = self.selector.predict_performance(&features, strategy);
632                (strategy, prediction)
633            })
634            .collect();
635
636        // Sort by quality score
637        recommendations.sort_by(|a, b| {
638            b.1.quality_score
639                .partial_cmp(&a.1.quality_score)
640                .unwrap_or(std::cmp::Ordering::Equal)
641        });
642
643        recommendations.into_iter().take(top_k).collect()
644    }
645
646    /// Get optimization statistics
647    #[must_use]
648    pub fn get_statistics(&self) -> MetaLearningStatistics {
649        MetaLearningStatistics {
650            total_optimizations: self.total_optimizations,
651            average_success_rate: self.average_success_rate,
652            history_size: self.history.len(),
653            exploration_rate: self.selector.exploration_rate,
654        }
655    }
656}
657
658/// Meta-learning statistics
659#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct MetaLearningStatistics {
661    pub total_optimizations: usize,
662    pub average_success_rate: f64,
663    pub history_size: usize,
664    pub exploration_rate: f64,
665}
666
667#[cfg(test)]
668mod tests {
669    use super::*;
670
671    #[test]
672    fn test_meta_learning_optimizer_creation() {
673        let optimizer = MetaLearningOptimizer::new(100, 42);
674        assert_eq!(optimizer.total_optimizations, 0);
675        assert_eq!(optimizer.average_success_rate, 0.0);
676    }
677
678    #[test]
679    fn test_feature_extraction() {
680        let optimizer = MetaLearningOptimizer::new(100, 42);
681        let mut model = IsingModel::new(5);
682        model.set_coupling(0, 1, -1.0).expect("should set coupling");
683        model.set_coupling(1, 2, -1.0).expect("should set coupling");
684        model.set_bias(0, 0.5).expect("should set bias");
685
686        let features = optimizer.extract_features(&model);
687        assert_eq!(features.num_variables, 5);
688        assert!(features.density > 0.0);
689    }
690
691    #[test]
692    fn test_strategy_selection() {
693        let mut optimizer = MetaLearningOptimizer::new(100, 42);
694        let mut model = IsingModel::new(10);
695        model.set_coupling(0, 1, -1.0).expect("should set coupling");
696
697        let strategy = optimizer.select_strategy(&model);
698        assert!(matches!(
699            strategy,
700            OptimizationStrategy::ClassicalAnnealing
701                | OptimizationStrategy::QuantumAnnealing
702                | OptimizationStrategy::PopulationAnnealing
703                | OptimizationStrategy::AdaptiveSchedule
704        ));
705    }
706
707    #[test]
708    fn test_performance_predictor() {
709        let predictor = PerformancePredictor::new(8);
710        let features = ProblemFeatures {
711            num_variables: 20,
712            density: 0.3,
713            coupling_mean: 1.0,
714            coupling_std: 0.5,
715            coupling_max: 2.0,
716            bias_mean: 0.0,
717            bias_std: 0.2,
718            average_degree: 6.0,
719            max_degree: 10,
720            clustering_coefficient: 0.3,
721            estimated_barriers: 0.5,
722            frustration_index: 0.4,
723            symmetry_score: 0.7,
724        };
725
726        let prediction = predictor.predict(&features, OptimizationStrategy::ClassicalAnnealing);
727        assert!(prediction.quality_score.abs() <= 1.0);
728        assert!(prediction.estimated_time.as_millis() > 0);
729    }
730
731    #[test]
732    fn test_transfer_learning() {
733        let mut engine = TransferLearningEngine::new();
734
735        let features1 = ProblemFeatures {
736            num_variables: 20,
737            density: 0.3,
738            coupling_mean: 1.0,
739            coupling_std: 0.5,
740            coupling_max: 2.0,
741            bias_mean: 0.0,
742            bias_std: 0.2,
743            average_degree: 6.0,
744            max_degree: 10,
745            clustering_coefficient: 0.3,
746            estimated_barriers: 0.5,
747            frustration_index: 0.4,
748            symmetry_score: 0.7,
749        };
750
751        let features2 = features1.clone();
752
753        let similarity = engine.compute_similarity(&features1, &features2);
754        assert!((similarity - 1.0).abs() < 0.01); // Should be very similar to itself
755    }
756
757    #[test]
758    fn test_record_optimization() {
759        let mut optimizer = MetaLearningOptimizer::new(100, 42);
760
761        let features = ProblemFeatures {
762            num_variables: 10,
763            density: 0.2,
764            coupling_mean: 1.0,
765            coupling_std: 0.3,
766            coupling_max: 1.5,
767            bias_mean: 0.0,
768            bias_std: 0.1,
769            average_degree: 4.0,
770            max_degree: 8,
771            clustering_coefficient: 0.2,
772            estimated_barriers: 0.3,
773            frustration_index: 0.3,
774            symmetry_score: 0.8,
775        };
776
777        let record = OptimizationRecord {
778            features,
779            strategy: OptimizationStrategy::ClassicalAnnealing,
780            parameters: HashMap::new(),
781            best_energy: -10.0,
782            convergence_time: Duration::from_secs(1),
783            iterations_to_converge: 100,
784            success_rate: 0.95,
785            cpu_time: Duration::from_secs(1),
786            memory_peak_mb: 50.0,
787            timestamp: Instant::now(),
788        };
789
790        optimizer.record_optimization(record);
791        assert_eq!(optimizer.total_optimizations, 1);
792        assert_eq!(optimizer.average_success_rate, 0.95);
793    }
794
795    #[test]
796    fn test_recommend_strategies() {
797        let mut optimizer = MetaLearningOptimizer::new(100, 42);
798        let mut model = IsingModel::new(15);
799        model.set_coupling(0, 1, -1.0).expect("should set coupling");
800        model.set_coupling(1, 2, 1.0).expect("should set coupling");
801
802        let recommendations = optimizer.recommend_strategies(&model, 3);
803        assert_eq!(recommendations.len(), 3);
804
805        // Should be sorted by quality
806        if recommendations.len() >= 2 {
807            assert!(recommendations[0].1.quality_score >= recommendations[1].1.quality_score);
808        }
809    }
810}