1use crate::error::{MLError, Result};
8use quantrs2_anneal::{ising::IsingModel, qubo::QuboBuilder, simulator::*};
9use scirs2_core::ndarray::{Array1, Array2};
10use std::collections::HashMap;
11
12pub struct QuantumMLQUBO {
14 qubo_matrix: Array2<f64>,
16 description: String,
18 variable_map: HashMap<String, usize>,
20 offset: f64,
22}
23
24impl QuantumMLQUBO {
25 pub fn new(size: usize, description: impl Into<String>) -> Self {
27 Self {
28 qubo_matrix: Array2::zeros((size, size)),
29 description: description.into(),
30 variable_map: HashMap::new(),
31 offset: 0.0,
32 }
33 }
34
35 pub fn set_coefficient(&mut self, i: usize, j: usize, value: f64) -> Result<()> {
37 if i >= self.qubo_matrix.nrows() || j >= self.qubo_matrix.ncols() {
38 return Err(MLError::InvalidConfiguration(
39 "Index out of bounds".to_string(),
40 ));
41 }
42 self.qubo_matrix[[i, j]] = value;
43 Ok(())
44 }
45
46 pub fn add_variable(&mut self, name: impl Into<String>, index: usize) {
48 self.variable_map.insert(name.into(), index);
49 }
50
51 pub fn qubo_matrix(&self) -> &Array2<f64> {
53 &self.qubo_matrix
54 }
55
56 pub fn to_ising(&self) -> IsingProblem {
58 let n = self.qubo_matrix.nrows();
60 let mut h = Array1::zeros(n);
61 let mut j = Array2::zeros((n, n));
62 let mut offset = self.offset;
63
64 for i in 0..n {
66 h[i] = self.qubo_matrix[[i, i]];
67 for k in 0..n {
68 if k != i {
69 h[i] += 0.5 * self.qubo_matrix[[i, k]];
70 }
71 }
72 offset += 0.5 * self.qubo_matrix[[i, i]];
73 }
74
75 for i in 0..n {
76 for k in i + 1..n {
77 j[[i, k]] = 0.25 * self.qubo_matrix[[i, k]];
78 }
79 }
80
81 IsingProblem::new(h, j, offset)
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct IsingProblem {
88 pub h: Array1<f64>,
90 pub j: Array2<f64>,
92 pub offset: f64,
94}
95
96impl IsingProblem {
97 pub fn new(h: Array1<f64>, j: Array2<f64>, offset: f64) -> Self {
99 Self { h, j, offset }
100 }
101
102 pub fn energy(&self, spins: &[i8]) -> f64 {
104 let mut energy = self.offset;
105
106 for (i, &spin) in spins.iter().enumerate() {
108 energy -= self.h[i] * spin as f64;
109 }
110
111 for i in 0..spins.len() {
113 for j in i + 1..spins.len() {
114 energy -= self.j[[i, j]] * spins[i] as f64 * spins[j] as f64;
115 }
116 }
117
118 energy
119 }
120}
121
122pub struct QuantumMLAnnealer {
124 params: AnnealingParams,
126 embedding: Option<Embedding>,
128 client: Option<Box<dyn AnnealingClient>>,
130}
131
132#[derive(Debug, Clone)]
134pub struct AnnealingParams {
135 pub num_sweeps: usize,
137 pub schedule: AnnealingSchedule,
139 pub temperature_range: (f64, f64),
141 pub num_chains: usize,
143 pub chain_strength: f64,
145}
146
147impl Default for AnnealingParams {
148 fn default() -> Self {
149 Self {
150 num_sweeps: 1000,
151 schedule: AnnealingSchedule::Linear,
152 temperature_range: (0.01, 10.0),
153 num_chains: 1,
154 chain_strength: 1.0,
155 }
156 }
157}
158
159#[derive(Debug, Clone)]
161pub enum AnnealingSchedule {
162 Linear,
164 Exponential,
166 Custom(Vec<f64>),
168}
169
170#[derive(Debug, Clone)]
172pub struct Embedding {
173 pub logical_to_physical: HashMap<usize, Vec<usize>>,
175 pub physical_to_logical: HashMap<usize, usize>,
177}
178
179impl Embedding {
180 pub fn identity(num_qubits: usize) -> Self {
182 let logical_to_physical: HashMap<usize, Vec<usize>> =
183 (0..num_qubits).map(|i| (i, vec![i])).collect();
184 let physical_to_logical: HashMap<usize, usize> = (0..num_qubits).map(|i| (i, i)).collect();
185
186 Self {
187 logical_to_physical,
188 physical_to_logical,
189 }
190 }
191
192 pub fn with_chains(chains: HashMap<usize, Vec<usize>>) -> Self {
194 let mut physical_to_logical = HashMap::new();
195 for (logical, physical_qubits) in &chains {
196 for &physical in physical_qubits {
197 physical_to_logical.insert(physical, *logical);
198 }
199 }
200
201 Self {
202 logical_to_physical: chains,
203 physical_to_logical,
204 }
205 }
206}
207
208pub trait AnnealingClient: Send + Sync {
210 fn solve_qubo(&self, qubo: &QuantumMLQUBO, params: &AnnealingParams)
212 -> Result<AnnealingResult>;
213
214 fn solve_ising(
216 &self,
217 ising: &IsingProblem,
218 params: &AnnealingParams,
219 ) -> Result<AnnealingResult>;
220
221 fn name(&self) -> &str;
223
224 fn capabilities(&self) -> AnnealingCapabilities;
226}
227
228#[derive(Debug, Clone)]
230pub struct AnnealingResult {
231 pub solution: Array1<i8>,
233 pub energy: f64,
235 pub num_evaluations: usize,
237 pub timing: AnnealingTiming,
239 pub metadata: HashMap<String, f64>,
241}
242
243#[derive(Debug, Clone)]
245pub struct AnnealingTiming {
246 pub total_time: std::time::Duration,
248 pub queue_time: Option<std::time::Duration>,
250 pub anneal_time: Option<std::time::Duration>,
252}
253
254#[derive(Debug, Clone)]
256pub struct AnnealingCapabilities {
257 pub max_variables: usize,
259 pub max_couplers: usize,
261 pub connectivity: ConnectivityGraph,
263 pub supported_problems: Vec<ProblemType>,
265}
266
267#[derive(Debug, Clone)]
269pub enum ConnectivityGraph {
270 Complete,
272 Chimera {
274 rows: usize,
275 cols: usize,
276 shore: usize,
277 },
278 Pegasus { size: usize },
280 Custom(Array2<bool>),
282}
283
284#[derive(Debug, Clone, Copy)]
286pub enum ProblemType {
287 QUBO,
289 Ising,
291 Constrained,
293}
294
295impl QuantumMLAnnealer {
296 pub fn new() -> Self {
298 Self {
299 params: AnnealingParams::default(),
300 embedding: None,
301 client: None,
302 }
303 }
304
305 pub fn with_params(mut self, params: AnnealingParams) -> Self {
307 self.params = params;
308 self
309 }
310
311 pub fn with_embedding(mut self, embedding: Embedding) -> Self {
313 self.embedding = Some(embedding);
314 self
315 }
316
317 pub fn with_client(mut self, client: Box<dyn AnnealingClient>) -> Self {
319 self.client = Some(client);
320 self
321 }
322
323 pub fn optimize(&self, problem: QuantumMLOptimizationProblem) -> Result<OptimizationResult> {
325 let qubo = self.convert_to_qubo(&problem)?;
327
328 let anneal_result = if let Some(ref client) = self.client {
330 client.solve_qubo(&qubo, &self.params)?
331 } else {
332 self.simulated_annealing(&qubo)?
334 };
335
336 self.convert_to_ml_solution(&problem, &anneal_result)
338 }
339
340 fn convert_to_qubo(&self, problem: &QuantumMLOptimizationProblem) -> Result<QuantumMLQUBO> {
342 match problem {
343 QuantumMLOptimizationProblem::FeatureSelection(fs_problem) => {
344 self.feature_selection_to_qubo(fs_problem)
345 }
346 QuantumMLOptimizationProblem::HyperparameterOptimization(hp_problem) => {
347 self.hyperparameter_to_qubo(hp_problem)
348 }
349 QuantumMLOptimizationProblem::CircuitOptimization(circuit_problem) => {
350 self.circuit_optimization_to_qubo(circuit_problem)
351 }
352 QuantumMLOptimizationProblem::PortfolioOptimization(portfolio_problem) => {
353 self.portfolio_to_qubo(portfolio_problem)
354 }
355 }
356 }
357
358 fn feature_selection_to_qubo(
360 &self,
361 problem: &FeatureSelectionProblem,
362 ) -> Result<QuantumMLQUBO> {
363 let num_features = problem.feature_importance.len();
364 let mut qubo = QuantumMLQUBO::new(num_features, "Feature Selection");
365
366 for i in 0..num_features {
368 qubo.set_coefficient(i, i, -problem.feature_importance[i])?;
369 qubo.add_variable(format!("feature_{}", i), i);
370 }
371
372 let penalty = problem.penalty_strength;
374 for i in 0..num_features {
375 for j in i + 1..num_features {
376 qubo.set_coefficient(i, j, penalty)?;
377 }
378 }
379
380 Ok(qubo)
381 }
382
383 fn hyperparameter_to_qubo(&self, problem: &HyperparameterProblem) -> Result<QuantumMLQUBO> {
385 let total_bits = problem
386 .parameter_encodings
387 .iter()
388 .map(|encoding| encoding.num_bits)
389 .sum();
390
391 let mut qubo = QuantumMLQUBO::new(total_bits, "Hyperparameter Optimization");
392
393 let mut bit_offset = 0;
395 for (param_idx, encoding) in problem.parameter_encodings.iter().enumerate() {
396 for bit in 0..encoding.num_bits {
397 qubo.add_variable(format!("param_{}_bit_{}", param_idx, bit), bit_offset + bit);
398 }
399 bit_offset += encoding.num_bits;
400 }
401
402 for i in 0..total_bits {
405 qubo.set_coefficient(i, i, fastrand::f64() - 0.5)?;
406 }
407
408 Ok(qubo)
409 }
410
411 fn circuit_optimization_to_qubo(
413 &self,
414 problem: &CircuitOptimizationProblem,
415 ) -> Result<QuantumMLQUBO> {
416 let num_positions = problem.gate_positions.len();
417 let num_gate_types = problem.gate_types.len();
418 let total_vars = num_positions * num_gate_types;
419
420 let mut qubo = QuantumMLQUBO::new(total_vars, "Circuit Optimization");
421
422 for pos in 0..num_positions {
424 for gate in 0..num_gate_types {
425 let var_idx = pos * num_gate_types + gate;
426 qubo.add_variable(format!("pos_{}_gate_{}", pos, gate), var_idx);
427
428 let cost = problem.gate_costs.get(&gate).copied().unwrap_or(1.0);
430 qubo.set_coefficient(var_idx, var_idx, cost)?;
431 }
432 }
433
434 let penalty = 10.0;
436 for pos in 0..num_positions {
437 for g1 in 0..num_gate_types {
438 for g2 in g1 + 1..num_gate_types {
439 let var1 = pos * num_gate_types + g1;
440 let var2 = pos * num_gate_types + g2;
441 qubo.set_coefficient(var1, var2, penalty)?;
442 }
443 }
444 }
445
446 Ok(qubo)
447 }
448
449 fn portfolio_to_qubo(&self, problem: &PortfolioOptimizationProblem) -> Result<QuantumMLQUBO> {
451 let num_assets = problem.expected_returns.len();
452 let mut qubo = QuantumMLQUBO::new(num_assets, "Portfolio Optimization");
453
454 for i in 0..num_assets {
456 qubo.set_coefficient(i, i, -problem.expected_returns[i])?;
458 qubo.add_variable(format!("asset_{}", i), i);
459 }
460
461 for i in 0..num_assets {
463 for j in i..num_assets {
464 let risk_penalty = problem.risk_aversion * problem.covariance_matrix[[i, j]];
465 qubo.set_coefficient(i, j, risk_penalty)?;
466 }
467 }
468
469 Ok(qubo)
470 }
471
472 fn simulated_annealing(&self, qubo: &QuantumMLQUBO) -> Result<AnnealingResult> {
474 let start_time = std::time::Instant::now();
475 let n = qubo.qubo_matrix.nrows();
476 let mut solution = Array1::from_vec(
477 (0..n)
478 .map(|_| if fastrand::bool() { 1 } else { -1 })
479 .collect(),
480 );
481 let mut best_energy = self.compute_qubo_energy(qubo, &solution);
482
483 let (t_start, t_end) = self.params.temperature_range;
484 let cooling_rate = (t_end / t_start).powf(1.0 / self.params.num_sweeps as f64);
485 let mut temperature = t_start;
486
487 for _sweep in 0..self.params.num_sweeps {
488 for i in 0..n {
489 solution[i] *= -1;
491 let new_energy = self.compute_qubo_energy(qubo, &solution);
492
493 if new_energy < best_energy
495 || fastrand::f64() < ((best_energy - new_energy) / temperature).exp()
496 {
497 best_energy = new_energy;
498 } else {
499 solution[i] *= -1;
501 }
502 }
503 temperature *= cooling_rate;
504 }
505
506 Ok(AnnealingResult {
507 solution,
508 energy: best_energy,
509 num_evaluations: self.params.num_sweeps * n,
510 timing: AnnealingTiming {
511 total_time: start_time.elapsed(),
512 queue_time: None,
513 anneal_time: Some(start_time.elapsed()),
514 },
515 metadata: HashMap::new(),
516 })
517 }
518
519 fn compute_qubo_energy(&self, qubo: &QuantumMLQUBO, solution: &Array1<i8>) -> f64 {
521 let mut energy = 0.0;
522 let n = solution.len();
523
524 for i in 0..n {
525 for j in 0..n {
526 energy += qubo.qubo_matrix[[i, j]] * (solution[i] as f64) * (solution[j] as f64);
527 }
528 }
529
530 energy
531 }
532
533 fn convert_to_ml_solution(
535 &self,
536 problem: &QuantumMLOptimizationProblem,
537 result: &AnnealingResult,
538 ) -> Result<OptimizationResult> {
539 match problem {
540 QuantumMLOptimizationProblem::FeatureSelection(_) => {
541 let selected_features: Vec<usize> = result
542 .solution
543 .iter()
544 .enumerate()
545 .filter_map(|(i, &val)| if val > 0 { Some(i) } else { None })
546 .collect();
547
548 Ok(OptimizationResult::FeatureSelection { selected_features })
549 }
550 QuantumMLOptimizationProblem::HyperparameterOptimization(hp_problem) => {
551 let mut parameters = Vec::new();
552 let mut bit_offset = 0;
553
554 for encoding in &hp_problem.parameter_encodings {
555 let mut param_value = 0;
556 for bit in 0..encoding.num_bits {
557 if result.solution[bit_offset + bit] > 0 {
558 param_value |= 1 << bit;
559 }
560 }
561 parameters.push(param_value as f64);
562 bit_offset += encoding.num_bits;
563 }
564
565 Ok(OptimizationResult::Hyperparameters { parameters })
566 }
567 QuantumMLOptimizationProblem::CircuitOptimization(circuit_problem) => {
568 let num_gate_types = circuit_problem.gate_types.len();
569 let mut circuit_design = Vec::new();
570
571 for pos in 0..circuit_problem.gate_positions.len() {
572 for gate in 0..num_gate_types {
573 let var_idx = pos * num_gate_types + gate;
574 if result.solution[var_idx] > 0 {
575 circuit_design.push(gate);
576 break;
577 }
578 }
579 }
580
581 Ok(OptimizationResult::CircuitDesign { circuit_design })
582 }
583 QuantumMLOptimizationProblem::PortfolioOptimization(_) => {
584 let portfolio: Vec<f64> = result
585 .solution
586 .iter()
587 .map(|&val| if val > 0 { 1.0 } else { 0.0 })
588 .collect();
589
590 Ok(OptimizationResult::Portfolio { weights: portfolio })
591 }
592 }
593 }
594}
595
596#[derive(Debug, Clone)]
598pub enum QuantumMLOptimizationProblem {
599 FeatureSelection(FeatureSelectionProblem),
601 HyperparameterOptimization(HyperparameterProblem),
603 CircuitOptimization(CircuitOptimizationProblem),
605 PortfolioOptimization(PortfolioOptimizationProblem),
607}
608
609#[derive(Debug, Clone)]
611pub struct FeatureSelectionProblem {
612 pub feature_importance: Array1<f64>,
614 pub penalty_strength: f64,
616 pub max_features: Option<usize>,
618}
619
620#[derive(Debug, Clone)]
622pub struct HyperparameterProblem {
623 pub parameter_encodings: Vec<ParameterEncoding>,
625 pub cv_function: String,
627}
628
629#[derive(Debug, Clone)]
631pub struct ParameterEncoding {
632 pub name: String,
634 pub num_bits: usize,
636 pub range: (f64, f64),
638}
639
640#[derive(Debug, Clone)]
642pub struct CircuitOptimizationProblem {
643 pub gate_positions: Vec<usize>,
645 pub gate_types: Vec<String>,
647 pub gate_costs: HashMap<usize, f64>,
649 pub connectivity: Array2<bool>,
651}
652
653#[derive(Debug, Clone)]
655pub struct PortfolioOptimizationProblem {
656 pub expected_returns: Array1<f64>,
658 pub covariance_matrix: Array2<f64>,
660 pub risk_aversion: f64,
662 pub budget: f64,
664}
665
666#[derive(Debug, Clone)]
668pub enum OptimizationResult {
669 FeatureSelection { selected_features: Vec<usize> },
671 Hyperparameters { parameters: Vec<f64> },
673 CircuitDesign { circuit_design: Vec<usize> },
675 Portfolio { weights: Vec<f64> },
677}
678
679pub struct DWaveClient {
681 token: String,
683 solver: String,
685 chain_strength: f64,
687}
688
689impl DWaveClient {
690 pub fn new(token: impl Into<String>, solver: impl Into<String>) -> Self {
692 Self {
693 token: token.into(),
694 solver: solver.into(),
695 chain_strength: 1.0,
696 }
697 }
698
699 pub fn with_chain_strength(mut self, strength: f64) -> Self {
701 self.chain_strength = strength;
702 self
703 }
704}
705
706impl AnnealingClient for DWaveClient {
707 fn solve_qubo(
708 &self,
709 qubo: &QuantumMLQUBO,
710 params: &AnnealingParams,
711 ) -> Result<AnnealingResult> {
712 let start_time = std::time::Instant::now();
714 let n = qubo.qubo_matrix.nrows();
715 let solution = Array1::from_vec(
716 (0..n)
717 .map(|_| if fastrand::bool() { 1 } else { -1 })
718 .collect(),
719 );
720 let energy = 0.0; Ok(AnnealingResult {
723 solution,
724 energy,
725 num_evaluations: params.num_sweeps,
726 timing: AnnealingTiming {
727 total_time: start_time.elapsed(),
728 queue_time: Some(std::time::Duration::from_millis(100)),
729 anneal_time: Some(std::time::Duration::from_millis(20)),
730 },
731 metadata: HashMap::new(),
732 })
733 }
734
735 fn solve_ising(
736 &self,
737 ising: &IsingProblem,
738 params: &AnnealingParams,
739 ) -> Result<AnnealingResult> {
740 let qubo = self.ising_to_qubo(ising);
742 self.solve_qubo(&qubo, params)
743 }
744
745 fn name(&self) -> &str {
746 "D-Wave"
747 }
748
749 fn capabilities(&self) -> AnnealingCapabilities {
750 AnnealingCapabilities {
751 max_variables: 5000,
752 max_couplers: 40000,
753 connectivity: ConnectivityGraph::Pegasus { size: 16 },
754 supported_problems: vec![ProblemType::QUBO, ProblemType::Ising],
755 }
756 }
757}
758
759impl DWaveClient {
760 fn ising_to_qubo(&self, ising: &IsingProblem) -> QuantumMLQUBO {
761 let n = ising.h.len();
762 let mut qubo = QuantumMLQUBO::new(n, "Ising to QUBO");
763
764 for i in 0..n {
766 qubo.set_coefficient(i, i, -2.0 * ising.h[i])
767 .expect("Index within bounds for diagonal coefficient");
768 }
769
770 for i in 0..n {
771 for j in i + 1..n {
772 qubo.set_coefficient(i, j, -4.0 * ising.j[[i, j]])
773 .expect("Index within bounds for off-diagonal coefficient");
774 }
775 }
776
777 qubo
778 }
779}
780
781pub mod anneal_utils {
783 use super::*;
784
785 pub fn create_feature_selection_problem(
787 num_features: usize,
788 max_features: usize,
789 ) -> FeatureSelectionProblem {
790 let feature_importance =
791 Array1::from_vec((0..num_features).map(|_| fastrand::f64()).collect());
792
793 FeatureSelectionProblem {
794 feature_importance,
795 penalty_strength: 0.1,
796 max_features: Some(max_features),
797 }
798 }
799
800 pub fn create_hyperparameter_problem(
802 param_names: Vec<String>,
803 param_ranges: Vec<(f64, f64)>,
804 bits_per_param: usize,
805 ) -> HyperparameterProblem {
806 let parameter_encodings = param_names
807 .into_iter()
808 .zip(param_ranges.into_iter())
809 .map(|(name, range)| ParameterEncoding {
810 name,
811 num_bits: bits_per_param,
812 range,
813 })
814 .collect();
815
816 HyperparameterProblem {
817 parameter_encodings,
818 cv_function: "accuracy".to_string(),
819 }
820 }
821
822 pub fn create_portfolio_problem(
824 num_assets: usize,
825 risk_aversion: f64,
826 ) -> PortfolioOptimizationProblem {
827 let expected_returns = Array1::from_vec(
828 (0..num_assets)
829 .map(|_| 0.05 + 0.1 * fastrand::f64())
830 .collect(),
831 );
832
833 let mut covariance_matrix = Array2::zeros((num_assets, num_assets));
834 for i in 0..num_assets {
835 for j in 0..num_assets {
836 let cov = if i == j {
837 0.01 + 0.02 * fastrand::f64()
838 } else {
839 0.005 * fastrand::f64()
840 };
841 covariance_matrix[[i, j]] = cov;
842 }
843 }
844
845 PortfolioOptimizationProblem {
846 expected_returns,
847 covariance_matrix,
848 risk_aversion,
849 budget: 1.0,
850 }
851 }
852}
853
854#[cfg(test)]
855mod tests {
856 use super::*;
857
858 #[test]
859 fn test_qubo_creation() {
860 let mut qubo = QuantumMLQUBO::new(3, "Test QUBO");
861 qubo.set_coefficient(0, 0, 1.0)
862 .expect("Failed to set coefficient (0,0)");
863 qubo.set_coefficient(0, 1, -2.0)
864 .expect("Failed to set coefficient (0,1)");
865
866 assert_eq!(qubo.qubo_matrix[[0, 0]], 1.0);
867 assert_eq!(qubo.qubo_matrix[[0, 1]], -2.0);
868 }
869
870 #[test]
871 fn test_ising_conversion() {
872 let mut qubo = QuantumMLQUBO::new(2, "Test");
873 qubo.set_coefficient(0, 0, 1.0)
874 .expect("Failed to set coefficient (0,0)");
875 qubo.set_coefficient(1, 1, -1.0)
876 .expect("Failed to set coefficient (1,1)");
877 qubo.set_coefficient(0, 1, 2.0)
878 .expect("Failed to set coefficient (0,1)");
879
880 let ising = qubo.to_ising();
881 assert_eq!(ising.h.len(), 2);
882 assert_eq!(ising.j.shape(), [2, 2]);
883 }
884
885 #[test]
886 fn test_annealer_creation() {
887 let annealer = QuantumMLAnnealer::new();
888 assert_eq!(annealer.params.num_sweeps, 1000);
889 }
890
891 #[test]
892 fn test_embedding() {
893 let embedding = Embedding::identity(5);
894 assert_eq!(embedding.logical_to_physical.len(), 5);
895 assert_eq!(embedding.physical_to_logical.len(), 5);
896 }
897
898 #[test]
899 fn test_feature_selection_problem() {
900 let problem = anneal_utils::create_feature_selection_problem(10, 5);
901 assert_eq!(problem.feature_importance.len(), 10);
902 assert_eq!(problem.max_features, Some(5));
903 }
904}