Skip to main content

quantrs2_tytan/
sampler_framework.rs

1//! Sampler framework extensions for advanced optimization strategies.
2//!
3//! This module provides plugin architecture, hyperparameter optimization,
4//! ensemble methods, and adaptive sampling strategies.
5
6#![allow(dead_code)]
7
8#[cfg(feature = "dwave")]
9use crate::compile::CompiledModel;
10use crate::sampler::{SampleResult, Sampler, SamplerError, SamplerResult};
11use scirs2_core::ndarray::{Array, Array2, IxDyn};
12use scirs2_core::random::prelude::*;
13use scirs2_core::random::prelude::*;
14use std::collections::HashMap;
15use std::sync::{Arc, Mutex};
16use std::time::{Duration, Instant};
17
18#[cfg(feature = "scirs")]
19use crate::scirs_stub::{
20    scirs2_ml::{CrossValidation, RandomForest},
21    scirs2_optimization::bayesian::{AcquisitionFunction, BayesianOptimizer, KernelType},
22};
23
24/// Plugin trait for custom samplers
25pub trait SamplerPlugin: Send + Sync {
26    /// Plugin name
27    fn name(&self) -> &str;
28
29    /// Plugin version
30    fn version(&self) -> &str;
31
32    /// Initialize plugin
33    fn initialize(&mut self, config: &HashMap<String, String>) -> Result<(), String>;
34
35    /// Create sampler instance
36    fn create_sampler(&self) -> Box<dyn Sampler>;
37
38    /// Get default configuration
39    fn default_config(&self) -> HashMap<String, String>;
40
41    /// Validate configuration
42    fn validate_config(&self, config: &HashMap<String, String>) -> Result<(), String>;
43}
44
45/// Plugin manager for dynamic sampler loading
46pub struct PluginManager {
47    /// Registered plugins
48    plugins: HashMap<String, Box<dyn SamplerPlugin>>,
49    /// Plugin configurations
50    configs: HashMap<String, HashMap<String, String>>,
51}
52
53impl Default for PluginManager {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl PluginManager {
60    /// Create new plugin manager
61    pub fn new() -> Self {
62        Self {
63            plugins: HashMap::new(),
64            configs: HashMap::new(),
65        }
66    }
67
68    /// Register a plugin
69    pub fn register_plugin(&mut self, plugin: Box<dyn SamplerPlugin>) -> Result<(), String> {
70        let name = plugin.name().to_string();
71
72        if self.plugins.contains_key(&name) {
73            return Err(format!("Plugin {name} already registered"));
74        }
75
76        let default_config = plugin.default_config();
77        self.configs.insert(name.clone(), default_config);
78        self.plugins.insert(name, plugin);
79
80        Ok(())
81    }
82
83    /// Configure plugin
84    pub fn configure_plugin(
85        &mut self,
86        name: &str,
87        config: HashMap<String, String>,
88    ) -> Result<(), String> {
89        let plugin = self
90            .plugins
91            .get(name)
92            .ok_or_else(|| format!("Plugin {name} not found"))?;
93
94        plugin.validate_config(&config)?;
95        self.configs.insert(name.to_string(), config);
96
97        Ok(())
98    }
99
100    /// Create sampler from plugin
101    pub fn create_sampler(&mut self, name: &str) -> Result<Box<dyn Sampler>, String> {
102        let plugin = self
103            .plugins
104            .get_mut(name)
105            .ok_or_else(|| format!("Plugin {name} not found"))?;
106
107        let config = self.configs.get(name).cloned().unwrap_or_default();
108        plugin.initialize(&config)?;
109
110        Ok(plugin.create_sampler())
111    }
112
113    /// List available plugins
114    pub fn list_plugins(&self) -> Vec<PluginInfo> {
115        self.plugins
116            .values()
117            .map(|p| PluginInfo {
118                name: p.name().to_string(),
119                version: p.version().to_string(),
120            })
121            .collect()
122    }
123}
124
125#[derive(Debug, Clone)]
126pub struct PluginInfo {
127    pub name: String,
128    pub version: String,
129}
130
131/// Hyperparameter optimization for samplers
132pub struct HyperparameterOptimizer {
133    /// Parameter search space
134    search_space: HashMap<String, ParameterSpace>,
135    /// Optimization method
136    method: OptimizationMethod,
137    /// Number of trials
138    num_trials: usize,
139    /// Cross-validation folds
140    cv_folds: usize,
141}
142
143#[derive(Debug, Clone)]
144pub enum ParameterSpace {
145    /// Continuous parameter
146    Continuous { min: f64, max: f64, log_scale: bool },
147    /// Discrete parameter
148    Discrete { values: Vec<f64> },
149    /// Categorical parameter
150    Categorical { options: Vec<String> },
151}
152
153#[derive(Debug, Clone)]
154pub enum OptimizationMethod {
155    /// Random search
156    RandomSearch,
157    /// Grid search
158    GridSearch { resolution: usize },
159    /// Bayesian optimization
160    #[cfg(feature = "scirs")]
161    Bayesian {
162        kernel: KernelType,
163        acquisition: AcquisitionFunction,
164        exploration: f64,
165    },
166    /// Evolutionary optimization
167    Evolutionary {
168        population_size: usize,
169        mutation_rate: f64,
170    },
171}
172
173impl HyperparameterOptimizer {
174    /// Create new optimizer
175    pub fn new(method: OptimizationMethod, num_trials: usize) -> Self {
176        Self {
177            search_space: HashMap::new(),
178            method,
179            num_trials,
180            cv_folds: 5,
181        }
182    }
183
184    /// Add parameter to search space
185    pub fn add_parameter(&mut self, name: &str, space: ParameterSpace) {
186        self.search_space.insert(name.to_string(), space);
187    }
188
189    /// Optimize hyperparameters
190    #[cfg(feature = "dwave")]
191    pub fn optimize<F>(
192        &self,
193        objective: F,
194        validation_problems: &[CompiledModel],
195    ) -> Result<OptimizationResult, String>
196    where
197        F: Fn(&HashMap<String, f64>) -> Box<dyn Sampler>,
198    {
199        match &self.method {
200            OptimizationMethod::RandomSearch => self.random_search(objective, validation_problems),
201            OptimizationMethod::GridSearch { resolution } => {
202                self.grid_search(objective, validation_problems, *resolution)
203            }
204            #[cfg(feature = "scirs")]
205            OptimizationMethod::Bayesian {
206                kernel,
207                acquisition,
208                exploration,
209            } => self.bayesian_optimization(
210                objective,
211                validation_problems,
212                *kernel,
213                *acquisition,
214                *exploration,
215            ),
216            OptimizationMethod::Evolutionary {
217                population_size,
218                mutation_rate,
219            } => self.evolutionary_optimization(
220                objective,
221                validation_problems,
222                *population_size,
223                *mutation_rate,
224            ),
225        }
226    }
227
228    /// Random search implementation
229    #[cfg(feature = "dwave")]
230    fn random_search<F>(
231        &self,
232        objective: F,
233        validation_problems: &[CompiledModel],
234    ) -> Result<OptimizationResult, String>
235    where
236        F: Fn(&HashMap<String, f64>) -> Box<dyn Sampler>,
237    {
238        let mut rng = thread_rng();
239        let mut best_params = HashMap::new();
240        let mut best_score = f64::INFINITY;
241        let mut history = Vec::new();
242
243        for trial in 0..self.num_trials {
244            // Sample random parameters
245            let mut params = self.sample_parameters(&mut rng)?;
246
247            // Evaluate
248            let sampler = objective(&params);
249            let mut score = self.evaluate_sampler(sampler, validation_problems)?;
250
251            history.push(TrialResult {
252                parameters: params.clone(),
253                score,
254                iteration: trial,
255            });
256
257            if score < best_score {
258                best_score = score;
259                best_params = params;
260            }
261        }
262
263        let convergence_curve = self.compute_convergence_curve(&history);
264        Ok(OptimizationResult {
265            best_parameters: best_params,
266            best_score,
267            history,
268            convergence_curve,
269        })
270    }
271
272    /// Grid search implementation
273    #[cfg(feature = "dwave")]
274    fn grid_search<F>(
275        &self,
276        objective: F,
277        validation_problems: &[CompiledModel],
278        resolution: usize,
279    ) -> Result<OptimizationResult, String>
280    where
281        F: Fn(&HashMap<String, f64>) -> Box<dyn Sampler>,
282    {
283        // Generate grid points
284        let grid_points = self.generate_grid(resolution)?;
285
286        let mut best_params = HashMap::new();
287        let mut best_score = f64::INFINITY;
288        let mut history = Vec::new();
289
290        for (i, params) in grid_points.iter().enumerate() {
291            let sampler = objective(params);
292            let mut score = self.evaluate_sampler(sampler, validation_problems)?;
293
294            history.push(TrialResult {
295                parameters: params.clone(),
296                score,
297                iteration: i,
298            });
299
300            if score < best_score {
301                best_score = score;
302                best_params = params.clone();
303            }
304        }
305
306        let convergence_curve = self.compute_convergence_curve(&history);
307        Ok(OptimizationResult {
308            best_parameters: best_params,
309            best_score,
310            history,
311            convergence_curve,
312        })
313    }
314
315    /// Bayesian optimization implementation
316    #[cfg(all(feature = "scirs", feature = "dwave"))]
317    fn bayesian_optimization<F>(
318        &self,
319        objective: F,
320        validation_problems: &[CompiledModel],
321        kernel: KernelType,
322        acquisition: AcquisitionFunction,
323        exploration: f64,
324    ) -> Result<OptimizationResult, String>
325    where
326        F: Fn(&HashMap<String, f64>) -> Box<dyn Sampler>,
327    {
328        use scirs2_core::ndarray::Array1;
329
330        let dim = self.search_space.len();
331        let mut optimizer = BayesianOptimizer::new(dim, kernel, acquisition, exploration)
332            .map_err(|e| e.to_string())?;
333
334        let mut history = Vec::new();
335        let mut x_data = Vec::new();
336        let mut y_data = Vec::new();
337
338        // Initial random samples
339        let mut rng = thread_rng();
340        for _ in 0..std::cmp::min(10, self.num_trials / 4) {
341            let mut params = self.sample_parameters(&mut rng)?;
342            let sampler = objective(&params);
343            let mut score = self.evaluate_sampler(sampler, validation_problems)?;
344
345            let mut x = self.params_to_array(&params)?;
346            x_data.push(x);
347            y_data.push(score);
348
349            history.push(TrialResult {
350                parameters: params,
351                score,
352                iteration: history.len(),
353            });
354        }
355
356        // Bayesian optimization loop
357        let y_array = Array1::from_vec(y_data.clone());
358        optimizer
359            .update(&x_data, &y_array)
360            .map_err(|e| e.to_string())?;
361
362        for _ in history.len()..self.num_trials {
363            // Suggest next point
364            let x_next = optimizer.suggest_next().map_err(|e| e.to_string())?;
365            let mut params = self.array_to_params(&x_next)?;
366
367            // Evaluate
368            let sampler = objective(&params);
369            let mut score = self.evaluate_sampler(sampler, validation_problems)?;
370
371            // Update model
372            x_data.push(x_next);
373            y_data.push(score);
374            let y_array = Array1::from_vec(y_data.clone());
375            optimizer
376                .update(&x_data, &y_array)
377                .map_err(|e| e.to_string())?;
378
379            history.push(TrialResult {
380                parameters: params,
381                score,
382                iteration: history.len(),
383            });
384        }
385
386        // Find best
387        let (best_idx, &best_score) = y_data
388            .iter()
389            .enumerate()
390            .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
391            .ok_or_else(|| "No optimization trials completed".to_string())?;
392
393        let best_params = self.array_to_params(&x_data[best_idx])?;
394
395        let convergence_curve = self.compute_convergence_curve(&history);
396        Ok(OptimizationResult {
397            best_parameters: best_params,
398            best_score,
399            history,
400            convergence_curve,
401        })
402    }
403
404    /// Evolutionary optimization using a standard genetic algorithm.
405    #[cfg(feature = "dwave")]
406    fn evolutionary_optimization<F>(
407        &self,
408        objective: F,
409        validation_problems: &[CompiledModel],
410        population_size: usize,
411        mutation_rate: f64,
412    ) -> Result<OptimizationResult, String>
413    where
414        F: Fn(&HashMap<String, f64>) -> Box<dyn Sampler>,
415    {
416        if population_size < 2 {
417            return Err("Population size must be at least 2".to_string());
418        }
419
420        let mut rng = thread_rng();
421        let max_generations = self.num_trials.max(1) / population_size.max(1);
422
423        // 1. Initialise population
424        let mut population: Vec<HashMap<String, f64>> = (0..population_size)
425            .map(|_| self.sample_parameters(&mut rng))
426            .collect::<Result<Vec<_>, _>>()?;
427
428        let mut history: Vec<TrialResult> = Vec::new();
429        let mut scores_per_gen: Vec<f64> = Vec::new();
430
431        for _gen in 0..max_generations {
432            // 2a. Evaluate each individual
433            let mut scored: Vec<(f64, HashMap<String, f64>)> = population
434                .into_iter()
435                .map(|params| {
436                    let sampler = objective(&params);
437                    let score = self
438                        .evaluate_sampler(sampler, validation_problems)
439                        .unwrap_or(f64::INFINITY);
440                    (score, params)
441                })
442                .collect();
443
444            // 2b. Sort by score ascending (lower is better)
445            scored.sort_by(|(sa, _), (sb, _)| {
446                sa.partial_cmp(sb).unwrap_or(std::cmp::Ordering::Equal)
447            });
448
449            // Record history
450            for (i, (score, params)) in scored.iter().enumerate() {
451                history.push(TrialResult {
452                    parameters: params.clone(),
453                    score: *score,
454                    iteration: history.len(),
455                });
456                if i == 0 {
457                    scores_per_gen.push(*score);
458                }
459            }
460
461            // 2c. Select top half as parents
462            let n_parents = population_size / 2;
463            let parents: Vec<HashMap<String, f64>> =
464                scored.into_iter().take(n_parents).map(|(_, p)| p).collect();
465
466            // 2d–e. Generate children via midpoint crossover + mutation
467            let mut children: Vec<HashMap<String, f64>> = Vec::new();
468            let mut pair_idx = 0usize;
469            while children.len() < population_size - n_parents {
470                let pa = &parents[pair_idx % n_parents];
471                let pb = &parents[(pair_idx + 1) % n_parents];
472                let mut child: HashMap<String, f64> = HashMap::new();
473                for key in pa.keys() {
474                    let va = pa.get(key).copied().unwrap_or(0.0);
475                    let vb = pb.get(key).copied().unwrap_or(0.0);
476                    let mid = (va + vb) / 2.0;
477                    let noise = rng.random_range(-mutation_rate..mutation_rate);
478                    child.insert(key.clone(), mid + noise);
479                }
480                children.push(child);
481                pair_idx += 1;
482            }
483
484            // 2f. Next generation = parents ∪ children (capped at population_size)
485            let mut next_gen = parents;
486            next_gen.extend(children);
487            next_gen.truncate(population_size);
488            population = next_gen;
489        }
490
491        // Evaluate final population
492        let mut best_params = population[0].clone();
493        let mut best_score = f64::INFINITY;
494        for params in &population {
495            let sampler = objective(params);
496            let score = self
497                .evaluate_sampler(sampler, validation_problems)
498                .unwrap_or(f64::INFINITY);
499            history.push(TrialResult {
500                parameters: params.clone(),
501                score,
502                iteration: history.len(),
503            });
504            if score < best_score {
505                best_score = score;
506                best_params = params.clone();
507            }
508        }
509
510        let convergence_curve = self.compute_convergence_curve(&history);
511        Ok(OptimizationResult {
512            best_parameters: best_params,
513            best_score,
514            history,
515            convergence_curve,
516        })
517    }
518
519    /// Sample parameters from search space
520    fn sample_parameters(&self, rng: &mut impl Rng) -> Result<HashMap<String, f64>, String> {
521        let mut params = HashMap::new();
522
523        for (name, space) in &self.search_space {
524            let value = match space {
525                ParameterSpace::Continuous {
526                    min,
527                    max,
528                    log_scale,
529                } => {
530                    if *log_scale {
531                        let log_min = min.ln();
532                        let log_max = max.ln();
533                        let log_val = rng.random_range(log_min..log_max);
534                        log_val.exp()
535                    } else {
536                        rng.random_range(*min..*max)
537                    }
538                }
539                ParameterSpace::Discrete { values } => values[rng.random_range(0..values.len())],
540                ParameterSpace::Categorical { options } => {
541                    // Return index for categorical
542                    rng.random_range(0..options.len()) as f64
543                }
544            };
545
546            params.insert(name.clone(), value);
547        }
548
549        Ok(params)
550    }
551
552    /// Generate grid points
553    fn generate_grid(&self, resolution: usize) -> Result<Vec<HashMap<String, f64>>, String> {
554        // Simplified: generate regular grid
555        let mut grid_points = Vec::new();
556
557        // This would need proper multi-dimensional grid generation
558        // For now, just sample uniformly
559        let total_points = resolution.pow(self.search_space.len() as u32);
560        let mut rng = thread_rng();
561
562        for _ in 0..total_points.min(self.num_trials) {
563            grid_points.push(self.sample_parameters(&mut rng)?);
564        }
565
566        Ok(grid_points)
567    }
568
569    /// Convert parameters to array
570    #[cfg(feature = "scirs")]
571    fn params_to_array(
572        &self,
573        params: &HashMap<String, f64>,
574    ) -> Result<scirs2_core::ndarray::Array1<f64>, String> {
575        let mut values = Vec::new();
576
577        // Ensure consistent ordering
578        let mut names: Vec<_> = self.search_space.keys().collect();
579        names.sort();
580
581        for name in names {
582            values.push(params.get(name).copied().unwrap_or(0.0));
583        }
584
585        Ok(scirs2_core::ndarray::Array1::from_vec(values))
586    }
587
588    /// Convert array to parameters
589    #[cfg(feature = "scirs")]
590    fn array_to_params(
591        &self,
592        array: &scirs2_core::ndarray::Array1<f64>,
593    ) -> Result<HashMap<String, f64>, String> {
594        let mut params = HashMap::new();
595
596        let mut names: Vec<_> = self.search_space.keys().collect();
597        names.sort();
598
599        for (i, name) in names.iter().enumerate() {
600            params.insert((*name).clone(), array[i]);
601        }
602
603        Ok(params)
604    }
605
606    /// Evaluate sampler performance
607    #[cfg(feature = "dwave")]
608    fn evaluate_sampler(
609        &self,
610        mut sampler: Box<dyn Sampler>,
611        problems: &[CompiledModel],
612    ) -> Result<f64, String> {
613        let mut scores = Vec::new();
614
615        for problem in problems {
616            let mut qubo = problem.to_qubo();
617            let start = Instant::now();
618
619            let qubo_tuple = (qubo.to_dense_matrix(), qubo.variable_map());
620            let mut results = sampler
621                .run_qubo(&qubo_tuple, 100)
622                .map_err(|e| format!("Sampler error: {e:?}"))?;
623
624            let elapsed = start.elapsed();
625
626            // Score based on solution quality and time
627            let mut best_energy = results.first().map_or(f64::INFINITY, |r| r.energy);
628
629            let time_penalty = elapsed.as_secs_f64();
630            let mut score = 0.1f64.mul_add(time_penalty, best_energy);
631
632            scores.push(score);
633        }
634
635        // Return average score
636        Ok(scores.iter().sum::<f64>() / scores.len() as f64)
637    }
638
639    /// Compute convergence curve
640    fn compute_convergence_curve(&self, history: &[TrialResult]) -> Vec<f64> {
641        let mut curve = Vec::new();
642        let mut best_so_far = f64::INFINITY;
643
644        for trial in history {
645            best_so_far = best_so_far.min(trial.score);
646            curve.push(best_so_far);
647        }
648
649        curve
650    }
651}
652
653#[derive(Debug, Clone)]
654pub struct OptimizationResult {
655    pub best_parameters: HashMap<String, f64>,
656    pub best_score: f64,
657    pub history: Vec<TrialResult>,
658    pub convergence_curve: Vec<f64>,
659}
660
661#[derive(Debug, Clone)]
662pub struct TrialResult {
663    pub parameters: HashMap<String, f64>,
664    pub score: f64,
665    pub iteration: usize,
666}
667
668/// Ensemble sampler that combines multiple sampling strategies
669pub struct EnsembleSampler {
670    /// Base samplers
671    samplers: Vec<Box<dyn Sampler>>,
672    /// Combination method
673    method: EnsembleMethod,
674    /// Weights for weighted combination
675    weights: Option<Vec<f64>>,
676}
677
678#[derive(Debug, Clone)]
679pub enum EnsembleMethod {
680    /// Simple voting
681    Voting,
682    /// Weighted voting
683    WeightedVoting,
684    /// Best of all
685    BestOf,
686    /// Sequential refinement
687    Sequential,
688    /// Parallel with aggregation
689    Parallel,
690}
691
692impl EnsembleSampler {
693    /// Create new ensemble sampler
694    pub fn new(samplers: Vec<Box<dyn Sampler>>, method: EnsembleMethod) -> Self {
695        Self {
696            samplers,
697            method,
698            weights: None,
699        }
700    }
701
702    /// Set weights for weighted voting
703    pub fn with_weights(mut self, weights: Vec<f64>) -> Self {
704        self.weights = Some(weights);
705        self
706    }
707}
708
709impl Sampler for EnsembleSampler {
710    fn run_qubo(
711        &self,
712        qubo: &(Array2<f64>, HashMap<String, usize>),
713        shots: usize,
714    ) -> SamplerResult<Vec<SampleResult>> {
715        match &self.method {
716            EnsembleMethod::Voting => self.voting_ensemble(qubo, shots),
717            EnsembleMethod::WeightedVoting => self.weighted_voting_ensemble(qubo, shots),
718            EnsembleMethod::BestOf => self.best_of_ensemble(qubo, shots),
719            EnsembleMethod::Sequential => self.sequential_ensemble(qubo, shots),
720            EnsembleMethod::Parallel => self.parallel_ensemble(qubo, shots),
721        }
722    }
723
724    fn run_hobo(
725        &self,
726        hobo: &(Array<f64, IxDyn>, HashMap<String, usize>),
727        shots: usize,
728    ) -> SamplerResult<Vec<SampleResult>> {
729        // Similar implementation for HOBO
730        match &self.method {
731            EnsembleMethod::Voting => self.voting_ensemble_hobo(hobo, shots),
732            _ => Err(SamplerError::InvalidParameter(
733                "HOBO ensemble not fully implemented".to_string(),
734            )),
735        }
736    }
737}
738
739impl EnsembleSampler {
740    /// Simple voting ensemble
741    fn voting_ensemble(
742        &self,
743        qubo: &(Array2<f64>, HashMap<String, usize>),
744        shots: usize,
745    ) -> SamplerResult<Vec<SampleResult>> {
746        let shots_per_sampler = shots / self.samplers.len();
747        let mut all_results = Vec::new();
748
749        // Run each sampler
750        for sampler in &self.samplers {
751            let results = sampler.run_qubo(qubo, shots_per_sampler)?;
752            all_results.extend(results);
753        }
754
755        // Aggregate by voting
756        let mut vote_counts: HashMap<Vec<bool>, (f64, usize)> = HashMap::new();
757
758        for result in all_results {
759            let state: Vec<bool> = qubo.1.keys().map(|var| result.assignments[var]).collect();
760
761            let entry = vote_counts.entry(state).or_insert((result.energy, 0));
762            entry.1 += result.occurrences;
763        }
764
765        // Convert back to results
766        let mut final_results: Vec<SampleResult> = vote_counts
767            .into_iter()
768            .map(|(state, (energy, count))| {
769                let assignments: HashMap<String, bool> = qubo
770                    .1
771                    .iter()
772                    .zip(state.iter())
773                    .map(|((var, _), &val)| (var.clone(), val))
774                    .collect();
775
776                SampleResult {
777                    assignments,
778                    energy,
779                    occurrences: count,
780                }
781            })
782            .collect();
783
784        final_results.sort_by(|a, b| {
785            a.energy
786                .partial_cmp(&b.energy)
787                .unwrap_or(std::cmp::Ordering::Equal)
788        });
789
790        Ok(final_results)
791    }
792
793    /// Weighted voting ensemble
794    fn weighted_voting_ensemble(
795        &self,
796        qubo: &(Array2<f64>, HashMap<String, usize>),
797        shots: usize,
798    ) -> SamplerResult<Vec<SampleResult>> {
799        let weights = self.weights.as_ref().ok_or_else(|| {
800            SamplerError::InvalidParameter("Weights not set for weighted voting".to_string())
801        })?;
802
803        if weights.len() != self.samplers.len() {
804            return Err(SamplerError::InvalidParameter(
805                "Number of weights must match number of samplers".to_string(),
806            ));
807        }
808
809        // Normalize weights
810        let total_weight: f64 = weights.iter().sum();
811        let normalized: Vec<f64> = weights.iter().map(|&w| w / total_weight).collect();
812
813        let mut all_results = Vec::new();
814
815        // Run each sampler with weighted shots
816        for (sampler, &weight) in self.samplers.iter().zip(normalized.iter()) {
817            let sampler_shots = (shots as f64 * weight).round() as usize;
818            if sampler_shots > 0 {
819                let results = sampler.run_qubo(qubo, sampler_shots)?;
820                all_results.extend(results);
821            }
822        }
823
824        // Aggregate results
825        self.aggregate_results(all_results, &qubo.1)
826    }
827
828    /// Best-of ensemble
829    fn best_of_ensemble(
830        &self,
831        qubo: &(Array2<f64>, HashMap<String, usize>),
832        shots: usize,
833    ) -> SamplerResult<Vec<SampleResult>> {
834        let shots_per_sampler = shots / self.samplers.len();
835        let mut best_results = Vec::new();
836        let mut best_energy = f64::INFINITY;
837
838        // Run each sampler and keep best
839        for sampler in &self.samplers {
840            let results = sampler.run_qubo(qubo, shots_per_sampler)?;
841
842            if let Some(best) = results.first() {
843                if best.energy < best_energy {
844                    best_energy = best.energy;
845                    best_results = results;
846                }
847            }
848        }
849
850        Ok(best_results)
851    }
852
853    /// Sequential refinement ensemble
854    fn sequential_ensemble(
855        &self,
856        qubo: &(Array2<f64>, HashMap<String, usize>),
857        shots: usize,
858    ) -> SamplerResult<Vec<SampleResult>> {
859        if self.samplers.is_empty() {
860            return Ok(Vec::new());
861        }
862
863        // Start with first sampler
864        let mut current_best = self.samplers[0].run_qubo(qubo, shots)?;
865
866        // Refine with subsequent samplers
867        for sampler in self.samplers.iter().skip(1) {
868            // Use best solutions as warm start (if sampler supports it)
869            // For now, just run independently
870            let refined = sampler.run_qubo(qubo, shots / self.samplers.len())?;
871
872            // Merge results
873            current_best.extend(refined);
874            current_best.sort_by(|a, b| {
875                a.energy
876                    .partial_cmp(&b.energy)
877                    .unwrap_or(std::cmp::Ordering::Equal)
878            });
879            current_best.truncate(shots);
880        }
881
882        Ok(current_best)
883    }
884
885    /// Parallel ensemble with aggregation
886    fn parallel_ensemble(
887        &self,
888        qubo: &(Array2<f64>, HashMap<String, usize>),
889        shots: usize,
890    ) -> SamplerResult<Vec<SampleResult>> {
891        let shots_per_sampler = shots / self.samplers.len();
892        let _handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
893
894        // Would need to make samplers thread-safe for real parallel execution
895        // For now, sequential execution
896        let mut all_results = Vec::new();
897
898        for sampler in &self.samplers {
899            let results = sampler.run_qubo(qubo, shots_per_sampler)?;
900            all_results.extend(results);
901        }
902
903        self.aggregate_results(all_results, &qubo.1)
904    }
905
906    /// Aggregate results from multiple samplers
907    fn aggregate_results(
908        &self,
909        results: Vec<SampleResult>,
910        var_map: &HashMap<String, usize>,
911    ) -> SamplerResult<Vec<SampleResult>> {
912        let mut aggregated: HashMap<Vec<bool>, (f64, usize)> = HashMap::new();
913
914        for result in results {
915            let state: Vec<bool> = var_map.keys().map(|var| result.assignments[var]).collect();
916
917            let entry = aggregated.entry(state).or_insert((result.energy, 0));
918
919            // Keep minimum energy for duplicates
920            entry.0 = entry.0.min(result.energy);
921            entry.1 += result.occurrences;
922        }
923
924        let mut final_results: Vec<SampleResult> = aggregated
925            .into_iter()
926            .map(|(state, (energy, count))| {
927                let assignments: HashMap<String, bool> = var_map
928                    .iter()
929                    .zip(state.iter())
930                    .map(|((var, _), &val)| (var.clone(), val))
931                    .collect();
932
933                SampleResult {
934                    assignments,
935                    energy,
936                    occurrences: count,
937                }
938            })
939            .collect();
940
941        final_results.sort_by(|a, b| {
942            a.energy
943                .partial_cmp(&b.energy)
944                .unwrap_or(std::cmp::Ordering::Equal)
945        });
946
947        Ok(final_results)
948    }
949
950    /// Voting ensemble for HOBO
951    fn voting_ensemble_hobo(
952        &self,
953        hobo: &(Array<f64, IxDyn>, HashMap<String, usize>),
954        shots: usize,
955    ) -> SamplerResult<Vec<SampleResult>> {
956        // Similar to QUBO voting but for HOBO
957        let shots_per_sampler = shots / self.samplers.len();
958        let mut all_results = Vec::new();
959
960        for sampler in &self.samplers {
961            let results = sampler.run_hobo(hobo, shots_per_sampler)?;
962            all_results.extend(results);
963        }
964
965        self.aggregate_results(all_results, &hobo.1)
966    }
967}
968
969/// Adaptive sampling strategy
970pub struct AdaptiveSampler<S: Sampler> {
971    /// Base sampler
972    base_sampler: S,
973    /// Adaptation strategy
974    strategy: AdaptationStrategy,
975    /// Performance history
976    history: Arc<Mutex<PerformanceHistory>>,
977}
978
979#[derive(Debug, Clone)]
980pub enum AdaptationStrategy {
981    /// Temperature adaptation
982    TemperatureAdaptive {
983        initial_range: (f64, f64),
984        adaptation_rate: f64,
985    },
986    /// Population size adaptation
987    PopulationAdaptive {
988        min_size: usize,
989        max_size: usize,
990        growth_rate: f64,
991    },
992    /// Multi-armed bandit for strategy selection
993    BanditAdaptive {
994        strategies: Vec<String>,
995        exploration_rate: f64,
996    },
997    /// Reinforcement learning based
998    RLAdaptive {
999        state_features: Vec<String>,
1000        action_space: Vec<String>,
1001    },
1002}
1003
1004#[derive(Default)]
1005struct PerformanceHistory {
1006    energies: Vec<f64>,
1007    times: Vec<Duration>,
1008    improvements: Vec<f64>,
1009    parameters: Vec<HashMap<String, f64>>,
1010}
1011
1012impl<S: Sampler> AdaptiveSampler<S> {
1013    /// Create new adaptive sampler
1014    pub fn new(base_sampler: S, strategy: AdaptationStrategy) -> Self {
1015        Self {
1016            base_sampler,
1017            strategy,
1018            history: Arc::new(Mutex::new(PerformanceHistory::default())),
1019        }
1020    }
1021
1022    /// Adapt parameters based on performance
1023    fn adapt_parameters(&self) -> HashMap<String, f64> {
1024        let history = self
1025            .history
1026            .lock()
1027            .unwrap_or_else(|poisoned| poisoned.into_inner());
1028
1029        match &self.strategy {
1030            AdaptationStrategy::TemperatureAdaptive {
1031                initial_range,
1032                adaptation_rate,
1033            } => {
1034                let mut params = HashMap::new();
1035
1036                // Adapt temperature based on acceptance rate
1037                let (min_temp, max_temp) = initial_range;
1038                let temp = if history.improvements.len() > 10 {
1039                    let recent_improvements: f64 =
1040                        history.improvements.iter().rev().take(10).sum::<f64>() / 10.0;
1041
1042                    if recent_improvements < 0.1 {
1043                        // Low improvement: increase temperature
1044                        min_temp + (max_temp - min_temp) * (1.0 - adaptation_rate)
1045                    } else {
1046                        // Good improvement: decrease temperature
1047                        min_temp + (max_temp - min_temp) * adaptation_rate
1048                    }
1049                } else {
1050                    (min_temp + max_temp) / 2.0
1051                };
1052
1053                params.insert("temperature".to_string(), temp);
1054                params
1055            }
1056            _ => HashMap::new(),
1057        }
1058    }
1059}
1060
1061impl<S: Sampler> Sampler for AdaptiveSampler<S> {
1062    fn run_qubo(
1063        &self,
1064        qubo: &(Array2<f64>, HashMap<String, usize>),
1065        shots: usize,
1066    ) -> SamplerResult<Vec<SampleResult>> {
1067        // Adapt parameters
1068        let params = self.adapt_parameters();
1069
1070        // Run base sampler (would need to apply params)
1071        let start = Instant::now();
1072        let results = self.base_sampler.run_qubo(qubo, shots)?;
1073        let elapsed = start.elapsed();
1074
1075        // Update history
1076        if let Some(best) = results.first() {
1077            let mut history = self
1078                .history
1079                .lock()
1080                .unwrap_or_else(|poisoned| poisoned.into_inner());
1081
1082            let improvement = if let Some(&last) = history.energies.last() {
1083                (last - best.energy) / last.abs().max(1.0)
1084            } else {
1085                1.0
1086            };
1087
1088            history.energies.push(best.energy);
1089            history.times.push(elapsed);
1090            history.improvements.push(improvement);
1091            history.parameters.push(params);
1092        }
1093
1094        Ok(results)
1095    }
1096
1097    fn run_hobo(
1098        &self,
1099        hobo: &(Array<f64, IxDyn>, HashMap<String, usize>),
1100        shots: usize,
1101    ) -> SamplerResult<Vec<SampleResult>> {
1102        // Similar adaptation for HOBO
1103        self.base_sampler.run_hobo(hobo, shots)
1104    }
1105}
1106
1107/// Cross-validation for sampler evaluation
1108pub struct SamplerCrossValidation {
1109    /// Number of folds
1110    n_folds: usize,
1111    /// Evaluation metric
1112    metric: EvaluationMetric,
1113}
1114
1115#[derive(Debug, Clone)]
1116pub enum EvaluationMetric {
1117    /// Best energy found
1118    BestEnergy,
1119    /// Average of top-k energies
1120    TopKAverage(usize),
1121    /// Time to solution
1122    TimeToSolution(f64),
1123    /// Success probability
1124    SuccessProbability(f64),
1125}
1126
1127impl SamplerCrossValidation {
1128    /// Create new cross-validation
1129    pub const fn new(n_folds: usize, metric: EvaluationMetric) -> Self {
1130        Self { n_folds, metric }
1131    }
1132
1133    /// Evaluate sampler with cross-validation
1134    #[cfg(feature = "dwave")]
1135    pub fn evaluate<S: Sampler>(
1136        &self,
1137        sampler: &S,
1138        problems: &[CompiledModel],
1139        shots_per_problem: usize,
1140    ) -> Result<CrossValidationResult, String> {
1141        let n_problems = problems.len();
1142        let fold_size = n_problems / self.n_folds;
1143
1144        let mut fold_scores = Vec::new();
1145
1146        for fold in 0..self.n_folds {
1147            let test_start = fold * fold_size;
1148            let test_end = if fold == self.n_folds - 1 {
1149                n_problems
1150            } else {
1151                (fold + 1) * fold_size
1152            };
1153
1154            let test_problems = &problems[test_start..test_end];
1155
1156            // Evaluate on test fold
1157            let mut scores = Vec::new();
1158            for problem in test_problems {
1159                let mut score = self.evaluate_single(sampler, problem, shots_per_problem)?;
1160                scores.push(score);
1161            }
1162
1163            let fold_score = scores.iter().sum::<f64>() / scores.len() as f64;
1164            fold_scores.push(fold_score);
1165        }
1166
1167        let mean_score = fold_scores.iter().sum::<f64>() / fold_scores.len() as f64;
1168        let variance = fold_scores
1169            .iter()
1170            .map(|&s| (s - mean_score).powi(2))
1171            .sum::<f64>()
1172            / fold_scores.len() as f64;
1173
1174        Ok(CrossValidationResult {
1175            mean_score,
1176            std_error: variance.sqrt(),
1177            fold_scores,
1178        })
1179    }
1180
1181    /// Evaluate single problem
1182    #[cfg(feature = "dwave")]
1183    fn evaluate_single<S: Sampler>(
1184        &self,
1185        sampler: &S,
1186        problem: &CompiledModel,
1187        shots: usize,
1188    ) -> Result<f64, String> {
1189        let mut qubo = problem.to_qubo();
1190        let qubo_tuple = (qubo.to_dense_matrix(), qubo.variable_map());
1191        let start = Instant::now();
1192        let mut results = sampler
1193            .run_qubo(&qubo_tuple, shots)
1194            .map_err(|e| format!("Sampler error: {e:?}"))?;
1195        let elapsed = start.elapsed();
1196
1197        match &self.metric {
1198            EvaluationMetric::BestEnergy => Ok(results.first().map_or(f64::INFINITY, |r| r.energy)),
1199            EvaluationMetric::TopKAverage(k) => {
1200                let sum: f64 = results.iter().take(*k).map(|r| r.energy).sum();
1201                Ok(sum / (*k).min(results.len()) as f64)
1202            }
1203            EvaluationMetric::TimeToSolution(threshold) => {
1204                let found = results.iter().any(|r| r.energy <= *threshold);
1205                Ok(if found {
1206                    elapsed.as_secs_f64()
1207                } else {
1208                    f64::INFINITY
1209                })
1210            }
1211            EvaluationMetric::SuccessProbability(threshold) => {
1212                let successes = results
1213                    .iter()
1214                    .filter(|r| r.energy <= *threshold)
1215                    .map(|r| r.occurrences)
1216                    .sum::<usize>();
1217                Ok(successes as f64 / shots as f64)
1218            }
1219        }
1220    }
1221}
1222
1223#[derive(Debug, Clone)]
1224pub struct CrossValidationResult {
1225    pub mean_score: f64,
1226    pub std_error: f64,
1227    pub fold_scores: Vec<f64>,
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232    use super::*;
1233    use crate::sampler::SASampler;
1234
1235    #[test]
1236    fn test_plugin_manager() {
1237        let manager = PluginManager::new();
1238
1239        // Would need actual plugin implementation to test
1240        assert_eq!(manager.list_plugins().len(), 0);
1241    }
1242
1243    #[test]
1244    fn test_hyperparameter_space() {
1245        let mut optimizer = HyperparameterOptimizer::new(OptimizationMethod::RandomSearch, 10);
1246
1247        optimizer.add_parameter(
1248            "temperature",
1249            ParameterSpace::Continuous {
1250                min: 0.1,
1251                max: 10.0,
1252                log_scale: true,
1253            },
1254        );
1255
1256        optimizer.add_parameter(
1257            "sweeps",
1258            ParameterSpace::Discrete {
1259                values: vec![100.0, 500.0, 1000.0],
1260            },
1261        );
1262
1263        // Would need actual optimization to test further
1264    }
1265
1266    #[test]
1267    fn test_ensemble_sampler() {
1268        let samplers: Vec<Box<dyn Sampler>> = vec![
1269            Box::new(SASampler::new(Some(42))),
1270            Box::new(SASampler::new(Some(43))),
1271        ];
1272
1273        let ensemble = EnsembleSampler::new(samplers, EnsembleMethod::Voting);
1274
1275        // Would need QUBO problem to test
1276    }
1277
1278    /// Test that evolutionary_optimization returns a valid result with trivial config.
1279    /// Uses an empty validation_problems slice so evaluate_sampler returns 0.0 (empty mean).
1280    #[cfg(feature = "dwave")]
1281    #[test]
1282    fn test_evolutionary_optimization_returns_params() {
1283        let mut optimizer = HyperparameterOptimizer::new(
1284            OptimizationMethod::Evolutionary {
1285                population_size: 4,
1286                mutation_rate: 0.1,
1287            },
1288            20, // num_trials → max_generations = 20/4 = 5
1289        );
1290        optimizer.add_parameter(
1291            "temperature",
1292            ParameterSpace::Continuous {
1293                min: 0.1,
1294                max: 5.0,
1295                log_scale: false,
1296            },
1297        );
1298
1299        let result = optimizer.optimize(
1300            |_params| Box::new(SASampler::new(None)),
1301            &[], // empty problems — evaluate_sampler returns average of empty = 0.0
1302        );
1303        assert!(
1304            result.is_ok(),
1305            "evolutionary_optimization should succeed: {:?}",
1306            result.err()
1307        );
1308        let opt = result.expect("expected Ok");
1309        assert!(
1310            !opt.best_parameters.is_empty(),
1311            "best_parameters should be non-empty"
1312        );
1313    }
1314}