1#![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
24pub trait SamplerPlugin: Send + Sync {
26 fn name(&self) -> &str;
28
29 fn version(&self) -> &str;
31
32 fn initialize(&mut self, config: &HashMap<String, String>) -> Result<(), String>;
34
35 fn create_sampler(&self) -> Box<dyn Sampler>;
37
38 fn default_config(&self) -> HashMap<String, String>;
40
41 fn validate_config(&self, config: &HashMap<String, String>) -> Result<(), String>;
43}
44
45pub struct PluginManager {
47 plugins: HashMap<String, Box<dyn SamplerPlugin>>,
49 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 pub fn new() -> Self {
62 Self {
63 plugins: HashMap::new(),
64 configs: HashMap::new(),
65 }
66 }
67
68 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 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 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 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
131pub struct HyperparameterOptimizer {
133 search_space: HashMap<String, ParameterSpace>,
135 method: OptimizationMethod,
137 num_trials: usize,
139 cv_folds: usize,
141}
142
143#[derive(Debug, Clone)]
144pub enum ParameterSpace {
145 Continuous { min: f64, max: f64, log_scale: bool },
147 Discrete { values: Vec<f64> },
149 Categorical { options: Vec<String> },
151}
152
153#[derive(Debug, Clone)]
154pub enum OptimizationMethod {
155 RandomSearch,
157 GridSearch { resolution: usize },
159 #[cfg(feature = "scirs")]
161 Bayesian {
162 kernel: KernelType,
163 acquisition: AcquisitionFunction,
164 exploration: f64,
165 },
166 Evolutionary {
168 population_size: usize,
169 mutation_rate: f64,
170 },
171}
172
173impl HyperparameterOptimizer {
174 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 pub fn add_parameter(&mut self, name: &str, space: ParameterSpace) {
186 self.search_space.insert(name.to_string(), space);
187 }
188
189 #[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 #[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 let mut params = self.sample_parameters(&mut rng)?;
246
247 let sampler = objective(¶ms);
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 #[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 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 #[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 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(¶ms);
343 let mut score = self.evaluate_sampler(sampler, validation_problems)?;
344
345 let mut x = self.params_to_array(¶ms)?;
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 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 let x_next = optimizer.suggest_next().map_err(|e| e.to_string())?;
365 let mut params = self.array_to_params(&x_next)?;
366
367 let sampler = objective(¶ms);
369 let mut score = self.evaluate_sampler(sampler, validation_problems)?;
370
371 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 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 #[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 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 let mut scored: Vec<(f64, HashMap<String, f64>)> = population
434 .into_iter()
435 .map(|params| {
436 let sampler = objective(¶ms);
437 let score = self
438 .evaluate_sampler(sampler, validation_problems)
439 .unwrap_or(f64::INFINITY);
440 (score, params)
441 })
442 .collect();
443
444 scored.sort_by(|(sa, _), (sb, _)| {
446 sa.partial_cmp(sb).unwrap_or(std::cmp::Ordering::Equal)
447 });
448
449 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 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 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 let mut next_gen = parents;
486 next_gen.extend(children);
487 next_gen.truncate(population_size);
488 population = next_gen;
489 }
490
491 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 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 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 fn generate_grid(&self, resolution: usize) -> Result<Vec<HashMap<String, f64>>, String> {
554 let mut grid_points = Vec::new();
556
557 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 #[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 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 #[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 #[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 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 Ok(scores.iter().sum::<f64>() / scores.len() as f64)
637 }
638
639 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
668pub struct EnsembleSampler {
670 samplers: Vec<Box<dyn Sampler>>,
672 method: EnsembleMethod,
674 weights: Option<Vec<f64>>,
676}
677
678#[derive(Debug, Clone)]
679pub enum EnsembleMethod {
680 Voting,
682 WeightedVoting,
684 BestOf,
686 Sequential,
688 Parallel,
690}
691
692impl EnsembleSampler {
693 pub fn new(samplers: Vec<Box<dyn Sampler>>, method: EnsembleMethod) -> Self {
695 Self {
696 samplers,
697 method,
698 weights: None,
699 }
700 }
701
702 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 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 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 for sampler in &self.samplers {
751 let results = sampler.run_qubo(qubo, shots_per_sampler)?;
752 all_results.extend(results);
753 }
754
755 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 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 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 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 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 self.aggregate_results(all_results, &qubo.1)
826 }
827
828 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 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 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 let mut current_best = self.samplers[0].run_qubo(qubo, shots)?;
865
866 for sampler in self.samplers.iter().skip(1) {
868 let refined = sampler.run_qubo(qubo, shots / self.samplers.len())?;
871
872 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 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 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 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 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 fn voting_ensemble_hobo(
952 &self,
953 hobo: &(Array<f64, IxDyn>, HashMap<String, usize>),
954 shots: usize,
955 ) -> SamplerResult<Vec<SampleResult>> {
956 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
969pub struct AdaptiveSampler<S: Sampler> {
971 base_sampler: S,
973 strategy: AdaptationStrategy,
975 history: Arc<Mutex<PerformanceHistory>>,
977}
978
979#[derive(Debug, Clone)]
980pub enum AdaptationStrategy {
981 TemperatureAdaptive {
983 initial_range: (f64, f64),
984 adaptation_rate: f64,
985 },
986 PopulationAdaptive {
988 min_size: usize,
989 max_size: usize,
990 growth_rate: f64,
991 },
992 BanditAdaptive {
994 strategies: Vec<String>,
995 exploration_rate: f64,
996 },
997 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 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 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 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 min_temp + (max_temp - min_temp) * (1.0 - adaptation_rate)
1045 } else {
1046 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 let params = self.adapt_parameters();
1069
1070 let start = Instant::now();
1072 let results = self.base_sampler.run_qubo(qubo, shots)?;
1073 let elapsed = start.elapsed();
1074
1075 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 self.base_sampler.run_hobo(hobo, shots)
1104 }
1105}
1106
1107pub struct SamplerCrossValidation {
1109 n_folds: usize,
1111 metric: EvaluationMetric,
1113}
1114
1115#[derive(Debug, Clone)]
1116pub enum EvaluationMetric {
1117 BestEnergy,
1119 TopKAverage(usize),
1121 TimeToSolution(f64),
1123 SuccessProbability(f64),
1125}
1126
1127impl SamplerCrossValidation {
1128 pub const fn new(n_folds: usize, metric: EvaluationMetric) -> Self {
1130 Self { n_folds, metric }
1131 }
1132
1133 #[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 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 #[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 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 }
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 }
1277
1278 #[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, );
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 &[], );
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}