1use crate::prelude::SimulatorError;
10use ndarray::{Array1, Array2};
11use num_complex::Complex64;
12use scirs2_core::parallel_ops::*;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16use crate::device_noise_models::{DeviceNoiseSimulator, DeviceTopology};
17use crate::error::Result;
18use crate::scirs2_integration::SciRS2Backend;
19
20#[derive(Debug, Clone)]
22pub struct QuantumAnnealingConfig {
23 pub annealing_time: f64,
25 pub time_steps: usize,
27 pub schedule_type: AnnealingScheduleType,
29 pub problem_type: ProblemType,
31 pub topology: AnnealingTopology,
33 pub temperature: f64,
35 pub enable_noise: bool,
37 pub enable_thermal_fluctuations: bool,
39 pub enable_control_errors: bool,
41 pub enable_gauge_transformations: bool,
43 pub post_processing: PostProcessingConfig,
45}
46
47impl Default for QuantumAnnealingConfig {
48 fn default() -> Self {
49 Self {
50 annealing_time: 20.0, time_steps: 2000,
52 schedule_type: AnnealingScheduleType::DWave,
53 problem_type: ProblemType::Ising,
54 topology: AnnealingTopology::Chimera(16),
55 temperature: 0.015, enable_noise: true,
57 enable_thermal_fluctuations: true,
58 enable_control_errors: true,
59 enable_gauge_transformations: true,
60 post_processing: PostProcessingConfig::default(),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq)]
67pub enum AnnealingScheduleType {
68 Linear,
70 DWave,
72 Optimized,
74 CustomPause {
76 pause_start: f64,
77 pause_duration: f64,
78 },
79 NonMonotonic,
81 Reverse { reinitialize_point: f64 },
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum ProblemType {
88 Ising,
90 QUBO,
92 MaxCut,
94 GraphColoring,
96 TSP,
98 NumberPartitioning,
100 Custom(String),
102}
103
104#[derive(Debug, Clone, PartialEq)]
106pub enum AnnealingTopology {
107 Chimera(usize), Pegasus(usize),
111 Zephyr(usize),
113 Complete(usize),
115 Custom(DeviceTopology),
117}
118
119#[derive(Debug, Clone)]
121pub struct PostProcessingConfig {
122 pub enable_spin_reversal: bool,
124 pub enable_local_search: bool,
126 pub max_local_search_iterations: usize,
128 pub enable_majority_vote: bool,
130 pub majority_vote_reads: usize,
132 pub enable_energy_filtering: bool,
134}
135
136impl Default for PostProcessingConfig {
137 fn default() -> Self {
138 Self {
139 enable_spin_reversal: true,
140 enable_local_search: true,
141 max_local_search_iterations: 100,
142 enable_majority_vote: true,
143 majority_vote_reads: 1000,
144 enable_energy_filtering: true,
145 }
146 }
147}
148
149#[derive(Debug, Clone)]
151pub struct IsingProblem {
152 pub num_spins: usize,
154 pub h: Array1<f64>,
156 pub j: Array2<f64>,
158 pub offset: f64,
160 pub metadata: ProblemMetadata,
162}
163
164#[derive(Debug, Clone)]
166pub struct QUBOProblem {
167 pub num_variables: usize,
169 pub q: Array2<f64>,
171 pub linear: Array1<f64>,
173 pub offset: f64,
175 pub metadata: ProblemMetadata,
177}
178
179#[derive(Debug, Clone, Default)]
181pub struct ProblemMetadata {
182 pub name: Option<String>,
184 pub description: Option<String>,
186 pub optimal_energy: Option<f64>,
188 pub difficulty_score: Option<f64>,
190 pub variable_labels: Vec<String>,
192}
193
194impl IsingProblem {
195 pub fn new(num_spins: usize) -> Self {
197 Self {
198 num_spins,
199 h: Array1::zeros(num_spins),
200 j: Array2::zeros((num_spins, num_spins)),
201 offset: 0.0,
202 metadata: ProblemMetadata::default(),
203 }
204 }
205
206 pub fn set_h(&mut self, i: usize, value: f64) {
208 if i < self.num_spins {
209 self.h[i] = value;
210 }
211 }
212
213 pub fn set_j(&mut self, i: usize, j: usize, value: f64) {
215 if i < self.num_spins && j < self.num_spins {
216 self.j[[i, j]] = value;
217 self.j[[j, i]] = value; }
219 }
220
221 pub fn calculate_energy(&self, configuration: &[i8]) -> f64 {
223 if configuration.len() != self.num_spins {
224 return f64::INFINITY;
225 }
226
227 let mut energy = self.offset;
228
229 for i in 0..self.num_spins {
231 energy += self.h[i] * configuration[i] as f64;
232 }
233
234 for i in 0..self.num_spins {
236 for j in i + 1..self.num_spins {
237 energy += self.j[[i, j]] * configuration[i] as f64 * configuration[j] as f64;
238 }
239 }
240
241 energy
242 }
243
244 pub fn to_qubo(&self) -> QUBOProblem {
246 let num_vars = self.num_spins;
247 let mut q = Array2::zeros((num_vars, num_vars));
248 let mut linear = Array1::zeros(num_vars);
249 let mut offset = self.offset;
250
251 for i in 0..num_vars {
256 linear[i] += 2.0 * self.h[i];
258 offset -= self.h[i];
259
260 for j in i + 1..num_vars {
261 q[[i, j]] += 4.0 * self.j[[i, j]];
264 linear[i] -= 2.0 * self.j[[i, j]];
265 linear[j] -= 2.0 * self.j[[i, j]];
266 offset += self.j[[i, j]];
267 }
268 }
269
270 QUBOProblem {
271 num_variables: num_vars,
272 q,
273 linear,
274 offset,
275 metadata: self.metadata.clone(),
276 }
277 }
278
279 pub fn find_ground_state_brute_force(&self) -> (Vec<i8>, f64) {
281 if self.num_spins > 20 {
282 panic!("Brute force search only supported for <= 20 spins");
283 }
284
285 let mut best_config = vec![-1; self.num_spins];
286 let mut best_energy = f64::INFINITY;
287
288 for state in 0..(1 << self.num_spins) {
289 let mut config = vec![-1; self.num_spins];
290 for i in 0..self.num_spins {
291 if (state >> i) & 1 == 1 {
292 config[i] = 1;
293 }
294 }
295
296 let energy = self.calculate_energy(&config);
297 if energy < best_energy {
298 best_energy = energy;
299 best_config = config;
300 }
301 }
302
303 (best_config, best_energy)
304 }
305}
306
307impl QUBOProblem {
308 pub fn new(num_variables: usize) -> Self {
310 Self {
311 num_variables,
312 q: Array2::zeros((num_variables, num_variables)),
313 linear: Array1::zeros(num_variables),
314 offset: 0.0,
315 metadata: ProblemMetadata::default(),
316 }
317 }
318
319 pub fn calculate_energy(&self, configuration: &[u8]) -> f64 {
321 if configuration.len() != self.num_variables {
322 return f64::INFINITY;
323 }
324
325 let mut energy = self.offset;
326
327 for i in 0..self.num_variables {
329 energy += self.linear[i] * configuration[i] as f64;
330 }
331
332 for i in 0..self.num_variables {
334 for j in 0..self.num_variables {
335 if i != j {
336 energy += self.q[[i, j]] * configuration[i] as f64 * configuration[j] as f64;
337 }
338 }
339 }
340
341 energy
342 }
343
344 pub fn to_ising(&self) -> IsingProblem {
346 let num_spins = self.num_variables;
347 let mut h = Array1::zeros(num_spins);
348 let mut j = Array2::zeros((num_spins, num_spins));
349 let mut offset = self.offset;
350
351 for i in 0..num_spins {
353 h[i] = self.linear[i] / 2.0;
354 offset += self.linear[i] / 2.0;
355
356 for k in 0..num_spins {
357 if k != i {
358 h[i] += self.q[[i, k]] / 4.0;
359 offset += self.q[[i, k]] / 4.0;
360 }
361 }
362 }
363
364 for i in 0..num_spins {
365 for k in i + 1..num_spins {
366 j[[i, k]] = self.q[[i, k]] / 4.0;
367 }
368 }
369
370 IsingProblem {
371 num_spins,
372 h,
373 j,
374 offset,
375 metadata: self.metadata.clone(),
376 }
377 }
378}
379
380pub struct QuantumAnnealingSimulator {
382 config: QuantumAnnealingConfig,
384 current_problem: Option<IsingProblem>,
386 noise_simulator: Option<DeviceNoiseSimulator>,
388 backend: Option<SciRS2Backend>,
390 annealing_history: Vec<AnnealingSnapshot>,
392 solutions: Vec<AnnealingSolution>,
394 stats: AnnealingStats,
396}
397
398#[derive(Debug, Clone)]
400pub struct AnnealingSnapshot {
401 pub time: f64,
403 pub s: f64,
405 pub transverse_field: f64,
407 pub longitudinal_field: f64,
409 pub quantum_state: Option<Array1<Complex64>>,
411 pub classical_probabilities: Option<Array1<f64>>,
413 pub energy_expectation: f64,
415 pub temperature_factor: f64,
417}
418
419#[derive(Debug, Clone)]
421pub struct AnnealingSolution {
422 pub configuration: Vec<i8>,
424 pub energy: f64,
426 pub probability: f64,
428 pub num_occurrences: usize,
430 pub rank: usize,
432}
433
434#[derive(Debug, Clone, Default, Serialize, Deserialize)]
436pub struct AnnealingStats {
437 pub total_annealing_time_ms: f64,
439 pub num_annealing_runs: usize,
441 pub num_solutions_found: usize,
443 pub best_energy_found: f64,
445 pub success_probability: f64,
447 pub time_to_solution: TimeToSolutionStats,
449 pub noise_stats: NoiseStats,
451}
452
453#[derive(Debug, Clone, Default, Serialize, Deserialize)]
455pub struct TimeToSolutionStats {
456 pub median_tts: f64,
458 pub percentile_99_tts: f64,
460 pub success_rate: f64,
462}
463
464#[derive(Debug, Clone, Default, Serialize, Deserialize)]
466pub struct NoiseStats {
467 pub thermal_excitations: usize,
469 pub control_errors: usize,
471 pub decoherence_events: usize,
473}
474
475impl QuantumAnnealingSimulator {
476 pub fn new(config: QuantumAnnealingConfig) -> Result<Self> {
478 Ok(Self {
479 config,
480 current_problem: None,
481 noise_simulator: None,
482 backend: None,
483 annealing_history: Vec::new(),
484 solutions: Vec::new(),
485 stats: AnnealingStats::default(),
486 })
487 }
488
489 pub fn with_backend(mut self) -> Result<Self> {
491 self.backend = Some(SciRS2Backend::new());
492 Ok(self)
493 }
494
495 pub fn set_problem(&mut self, problem: IsingProblem) -> Result<()> {
497 let max_spins = match &self.config.topology {
499 AnnealingTopology::Chimera(size) => size * size * 8,
500 AnnealingTopology::Pegasus(size) => size * (size - 1) * 12,
501 AnnealingTopology::Zephyr(size) => size * size * 8,
502 AnnealingTopology::Complete(size) => *size,
503 AnnealingTopology::Custom(topology) => topology.num_qubits,
504 };
505
506 if problem.num_spins > max_spins {
507 return Err(SimulatorError::InvalidInput(format!(
508 "Problem size {} exceeds topology limit {}",
509 problem.num_spins, max_spins
510 )));
511 }
512
513 self.current_problem = Some(problem);
514 Ok(())
515 }
516
517 pub fn anneal(&mut self, num_reads: usize) -> Result<AnnealingResult> {
519 let problem = self
520 .current_problem
521 .as_ref()
522 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
523
524 let start_time = std::time::Instant::now();
525 self.solutions.clear();
526
527 for read in 0..num_reads {
528 let read_start = std::time::Instant::now();
529
530 let solution = self.single_anneal(read)?;
532 self.solutions.push(solution);
533
534 if read % 100 == 0 {
535 println!(
536 "Completed read {}/{}, time={:.2}ms",
537 read,
538 num_reads,
539 read_start.elapsed().as_secs_f64() * 1000.0
540 );
541 }
542 }
543
544 if self.config.post_processing.enable_majority_vote {
546 self.apply_majority_vote_post_processing()?;
547 }
548
549 if self.config.post_processing.enable_local_search {
550 self.apply_local_search_post_processing()?;
551 }
552
553 self.solutions
555 .sort_by(|a, b| a.energy.partial_cmp(&b.energy).unwrap());
556
557 for (rank, solution) in self.solutions.iter_mut().enumerate() {
559 solution.rank = rank;
560 }
561
562 self.compute_annealing_statistics()?;
564
565 let total_time = start_time.elapsed().as_secs_f64() * 1000.0;
566 self.stats.total_annealing_time_ms += total_time;
567 self.stats.num_annealing_runs += num_reads;
568
569 Ok(AnnealingResult {
570 solutions: self.solutions.clone(),
571 best_energy: self
572 .solutions
573 .first()
574 .map(|s| s.energy)
575 .unwrap_or(f64::INFINITY),
576 annealing_history: self.annealing_history.clone(),
577 total_time_ms: total_time,
578 success_probability: self.stats.success_probability,
579 time_to_solution: self.stats.time_to_solution.clone(),
580 })
581 }
582
583 fn single_anneal(&mut self, read_id: usize) -> Result<AnnealingSolution> {
585 let problem_num_spins = self.current_problem.as_ref().unwrap().num_spins;
586
587 let state_size = 1 << problem_num_spins.min(20); let mut quantum_state = if problem_num_spins <= 20 {
590 let mut state = Array1::zeros(state_size);
591 let amplitude = (1.0 / state_size as f64).sqrt();
593 state.fill(Complex64::new(amplitude, 0.0));
594 Some(state)
595 } else {
596 None };
598
599 let dt = self.config.annealing_time / self.config.time_steps as f64;
600 self.annealing_history.clear();
601
602 for step in 0..=self.config.time_steps {
604 let t = step as f64 * dt;
605 let s = self.schedule_function(t);
606
607 let (transverse_field, longitudinal_field) = self.calculate_field_strengths(s);
609
610 if let Some(ref mut state) = quantum_state {
612 self.apply_quantum_evolution(state, transverse_field, longitudinal_field, dt)?;
613
614 if self.config.enable_noise {
616 self.apply_annealing_noise(state, dt)?;
617 }
618 }
619
620 if step % (self.config.time_steps / 100) == 0 {
622 let snapshot = self.take_annealing_snapshot(
623 t,
624 s,
625 transverse_field,
626 longitudinal_field,
627 &quantum_state,
628 )?;
629 self.annealing_history.push(snapshot);
630 }
631 }
632
633 let final_configuration = if let Some(ref state) = quantum_state {
635 self.measure_final_state(state)?
636 } else {
637 let problem = self.current_problem.as_ref().unwrap();
639 self.classical_sampling(problem)?
640 };
641
642 let energy = self
643 .current_problem
644 .as_ref()
645 .unwrap()
646 .calculate_energy(&final_configuration);
647
648 Ok(AnnealingSolution {
649 configuration: final_configuration,
650 energy,
651 probability: 1.0 / (self.config.time_steps as f64), num_occurrences: 1,
653 rank: 0,
654 })
655 }
656
657 fn schedule_function(&self, t: f64) -> f64 {
659 let normalized_t = t / self.config.annealing_time;
660
661 match self.config.schedule_type {
662 AnnealingScheduleType::Linear => normalized_t,
663 AnnealingScheduleType::DWave => {
664 if normalized_t < 0.1 {
666 5.0 * normalized_t * normalized_t
667 } else if normalized_t < 0.9 {
668 0.05 + 0.9 * (normalized_t - 0.1) / 0.8
669 } else {
670 0.95 + 0.05 * (1.0 - (1.0 - normalized_t) * (1.0 - normalized_t) / 0.01)
671 }
672 }
673 AnnealingScheduleType::Optimized => {
674 self.optimized_schedule(normalized_t)
676 }
677 AnnealingScheduleType::CustomPause {
678 pause_start,
679 pause_duration,
680 } => {
681 if normalized_t >= pause_start && normalized_t <= pause_start + pause_duration {
682 pause_start } else if normalized_t > pause_start + pause_duration {
684 (normalized_t - pause_duration - pause_start) / (1.0 - pause_duration)
685 } else {
686 normalized_t / pause_start
687 }
688 }
689 AnnealingScheduleType::NonMonotonic => {
690 normalized_t
692 + 0.1
693 * (10.0 * std::f64::consts::PI * normalized_t).sin()
694 * (1.0 - normalized_t)
695 }
696 AnnealingScheduleType::Reverse { reinitialize_point } => {
697 if normalized_t < reinitialize_point {
698 1.0 } else {
700 1.0 - (normalized_t - reinitialize_point) / (1.0 - reinitialize_point)
701 }
702 }
703 }
704 }
705
706 fn optimized_schedule(&self, t: f64) -> f64 {
708 if t < 0.3 {
711 t * t / 0.09 * 0.3
712 } else if t < 0.7 {
713 0.3 + (t - 0.3) * 0.4 / 0.4
714 } else {
715 0.7 + (t - 0.7) * (t - 0.7) / 0.09 * 0.3
716 }
717 }
718
719 fn calculate_field_strengths(&self, s: f64) -> (f64, f64) {
721 let a_s = (1.0 - s) * 1.0; let b_s = s * 1.0; (a_s, b_s)
725 }
726
727 fn apply_quantum_evolution(
729 &mut self,
730 state: &mut Array1<Complex64>,
731 transverse_field: f64,
732 longitudinal_field: f64,
733 dt: f64,
734 ) -> Result<()> {
735 let problem = self.current_problem.as_ref().unwrap();
736 let num_spins = problem.num_spins;
737
738 let hamiltonian = self.build_annealing_hamiltonian(transverse_field, longitudinal_field)?;
740
741 let evolution_operator = self.compute_evolution_operator(&hamiltonian, dt)?;
743 *state = evolution_operator.dot(state);
744
745 let norm: f64 = state.iter().map(|x| x.norm_sqr()).sum::<f64>().sqrt();
747 if norm > 1e-15 {
748 state.mapv_inplace(|x| x / norm);
749 }
750
751 Ok(())
752 }
753
754 fn build_annealing_hamiltonian(
756 &self,
757 transverse_field: f64,
758 longitudinal_field: f64,
759 ) -> Result<Array2<Complex64>> {
760 let problem = self.current_problem.as_ref().unwrap();
761 let num_spins = problem.num_spins;
762 let dim = 1 << num_spins;
763 let mut hamiltonian = Array2::zeros((dim, dim));
764
765 for spin in 0..num_spins {
767 let sigma_x = self.build_sigma_x(spin, num_spins);
768 hamiltonian = hamiltonian - sigma_x.mapv(|x| x * transverse_field);
769 }
770
771 let problem_hamiltonian = self.build_problem_hamiltonian()?;
773 hamiltonian = hamiltonian + problem_hamiltonian.mapv(|x| x * longitudinal_field);
774
775 Ok(hamiltonian)
776 }
777
778 fn build_sigma_x(&self, target_spin: usize, num_spins: usize) -> Array2<Complex64> {
780 let dim = 1 << num_spins;
781 let mut sigma_x = Array2::zeros((dim, dim));
782
783 for i in 0..dim {
784 let j = i ^ (1 << target_spin); sigma_x[[i, j]] = Complex64::new(1.0, 0.0);
786 }
787
788 sigma_x
789 }
790
791 fn build_problem_hamiltonian(&self) -> Result<Array2<Complex64>> {
793 let problem = self.current_problem.as_ref().unwrap();
794 let num_spins = problem.num_spins;
795 let dim = 1 << num_spins;
796 let mut hamiltonian = Array2::zeros((dim, dim));
797
798 for i in 0..num_spins {
800 let sigma_z = self.build_sigma_z(i, num_spins);
801 hamiltonian = hamiltonian + sigma_z.mapv(|x| x * problem.h[i]);
802 }
803
804 for i in 0..num_spins {
806 for j in i + 1..num_spins {
807 if problem.j[[i, j]] != 0.0 {
808 let sigma_z_i = self.build_sigma_z(i, num_spins);
809 let sigma_z_j = self.build_sigma_z(j, num_spins);
810 let sigma_z_ij = sigma_z_i.dot(&sigma_z_j);
811 hamiltonian = hamiltonian + sigma_z_ij.mapv(|x| x * problem.j[[i, j]]);
812 }
813 }
814 }
815
816 for i in 0..dim {
818 hamiltonian[[i, i]] += Complex64::new(problem.offset, 0.0);
819 }
820
821 Ok(hamiltonian)
822 }
823
824 fn build_sigma_z(&self, target_spin: usize, num_spins: usize) -> Array2<Complex64> {
826 let dim = 1 << num_spins;
827 let mut sigma_z = Array2::zeros((dim, dim));
828
829 for i in 0..dim {
830 let sign = if (i >> target_spin) & 1 == 0 {
831 1.0
832 } else {
833 -1.0
834 };
835 sigma_z[[i, i]] = Complex64::new(sign, 0.0);
836 }
837
838 sigma_z
839 }
840
841 fn compute_evolution_operator(
843 &self,
844 hamiltonian: &Array2<Complex64>,
845 dt: f64,
846 ) -> Result<Array2<Complex64>> {
847 self.matrix_exponential(hamiltonian, -Complex64::new(0.0, dt))
849 }
850
851 fn matrix_exponential(
853 &self,
854 matrix: &Array2<Complex64>,
855 factor: Complex64,
856 ) -> Result<Array2<Complex64>> {
857 let dim = matrix.dim().0;
858 let scaled_matrix = matrix.mapv(|x| x * factor);
859
860 let mut result = Array2::eye(dim);
861 let mut term = Array2::eye(dim);
862
863 for n in 1..=15 {
864 term = term.dot(&scaled_matrix) / (n as f64);
866 let term_norm: f64 = term.iter().map(|x| x.norm_sqr()).sum::<f64>().sqrt();
867
868 result = result + &term;
869
870 if term_norm < 1e-12 {
871 break;
872 }
873 }
874
875 Ok(result)
876 }
877
878 fn apply_annealing_noise(&mut self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
880 if self.config.enable_thermal_fluctuations {
881 self.apply_thermal_noise(state, dt)?;
882 self.stats.noise_stats.thermal_excitations += 1;
883 }
884
885 if self.config.enable_control_errors {
886 self.apply_control_error_noise(state, dt)?;
887 self.stats.noise_stats.control_errors += 1;
888 }
889
890 self.apply_decoherence_noise(state, dt)?;
892 self.stats.noise_stats.decoherence_events += 1;
893
894 Ok(())
895 }
896
897 fn apply_thermal_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
899 let kb_t = 1.38e-23 * self.config.temperature; let thermal_energy = kb_t * dt * 1e6; for amplitude in state.iter_mut() {
904 let thermal_phase = fastrand::f64() * thermal_energy * 2.0 * std::f64::consts::PI;
905 *amplitude *= Complex64::new(0.0, thermal_phase).exp();
906 }
907
908 Ok(())
909 }
910
911 fn apply_control_error_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
913 let error_strength = 0.01; let problem = self.current_problem.as_ref().unwrap();
918 for spin in 0..problem.num_spins.min(10) {
919 if fastrand::f64() < error_strength * dt {
921 let error_angle = fastrand::f64() * 0.1; self.apply_single_spin_rotation(state, spin, error_angle)?;
923 }
924 }
925
926 Ok(())
927 }
928
929 fn apply_decoherence_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
931 let decoherence_rate = 1e-3; let decoherence_prob = decoherence_rate * dt;
933
934 for amplitude in state.iter_mut() {
935 if fastrand::f64() < decoherence_prob {
936 let phase = fastrand::f64() * 2.0 * std::f64::consts::PI;
938 *amplitude *= Complex64::new(0.0, phase).exp();
939 }
940 }
941
942 Ok(())
943 }
944
945 fn apply_single_spin_rotation(
947 &self,
948 state: &mut Array1<Complex64>,
949 spin: usize,
950 angle: f64,
951 ) -> Result<()> {
952 let problem = self.current_problem.as_ref().unwrap();
953 let spin_mask = 1 << spin;
954 let cos_half = (angle / 2.0).cos();
955 let sin_half = (angle / 2.0).sin();
956
957 for i in 0..state.len() {
958 if i & spin_mask == 0 {
959 let j = i | spin_mask;
960 if j < state.len() {
961 let amp_0 = state[i];
962 let amp_1 = state[j];
963
964 state[i] = cos_half * amp_0 - Complex64::new(0.0, sin_half) * amp_1;
965 state[j] = cos_half * amp_1 - Complex64::new(0.0, sin_half) * amp_0;
966 }
967 }
968 }
969
970 Ok(())
971 }
972
973 fn take_annealing_snapshot(
975 &self,
976 time: f64,
977 s: f64,
978 transverse_field: f64,
979 longitudinal_field: f64,
980 quantum_state: &Option<Array1<Complex64>>,
981 ) -> Result<AnnealingSnapshot> {
982 let energy_expectation = if let Some(state) = quantum_state {
983 self.calculate_energy_expectation(state)?
984 } else {
985 0.0
986 };
987
988 let temperature_factor = (-1.0 / (1.38e-23 * self.config.temperature)).exp();
989
990 Ok(AnnealingSnapshot {
991 time,
992 s,
993 transverse_field,
994 longitudinal_field,
995 quantum_state: quantum_state.clone(),
996 classical_probabilities: None,
997 energy_expectation,
998 temperature_factor,
999 })
1000 }
1001
1002 fn calculate_energy_expectation(&self, state: &Array1<Complex64>) -> Result<f64> {
1004 let problem = self.current_problem.as_ref().unwrap();
1005 let mut expectation = 0.0;
1006
1007 for (i, &litude) in state.iter().enumerate() {
1008 let prob = amplitude.norm_sqr();
1009
1010 let mut config = vec![-1; problem.num_spins];
1012 for spin in 0..problem.num_spins {
1013 if (i >> spin) & 1 == 1 {
1014 config[spin] = 1;
1015 }
1016 }
1017
1018 let energy = problem.calculate_energy(&config);
1019 expectation += prob * energy;
1020 }
1021
1022 Ok(expectation)
1023 }
1024
1025 fn measure_final_state(&self, state: &Array1<Complex64>) -> Result<Vec<i8>> {
1027 let problem = self.current_problem.as_ref().unwrap();
1028
1029 let probabilities: Vec<f64> = state.iter().map(|x| x.norm_sqr()).collect();
1031 let random_val = fastrand::f64();
1032
1033 let mut cumulative_prob = 0.0;
1034 for (i, &prob) in probabilities.iter().enumerate() {
1035 cumulative_prob += prob;
1036 if random_val < cumulative_prob {
1037 let mut config = vec![-1; problem.num_spins];
1039 for spin in 0..problem.num_spins {
1040 if (i >> spin) & 1 == 1 {
1041 config[spin] = 1;
1042 }
1043 }
1044 return Ok(config);
1045 }
1046 }
1047
1048 Ok(vec![-1; problem.num_spins])
1050 }
1051
1052 fn classical_sampling(&self, problem: &IsingProblem) -> Result<Vec<i8>> {
1054 let mut config: Vec<i8> = (0..problem.num_spins)
1056 .map(|_| if fastrand::f64() > 0.5 { 1 } else { -1 })
1057 .collect();
1058
1059 for _ in 0..1000 {
1061 let spin_to_flip = fastrand::usize(0..problem.num_spins);
1062 let old_energy = problem.calculate_energy(&config);
1063
1064 config[spin_to_flip] *= -1;
1065 let new_energy = problem.calculate_energy(&config);
1066
1067 if new_energy > old_energy {
1068 config[spin_to_flip] *= -1; }
1070 }
1071
1072 Ok(config)
1073 }
1074
1075 fn apply_majority_vote_post_processing(&mut self) -> Result<()> {
1077 if self.solutions.is_empty() {
1078 return Ok(());
1079 }
1080
1081 let mut config_groups: HashMap<Vec<i8>, Vec<usize>> = HashMap::new();
1083 for (i, solution) in self.solutions.iter().enumerate() {
1084 config_groups
1085 .entry(solution.configuration.clone())
1086 .or_insert_with(Vec::new)
1087 .push(i);
1088 }
1089
1090 for (config, indices) in config_groups {
1092 let num_occurrences = indices.len();
1093 for &idx in &indices {
1094 self.solutions[idx].num_occurrences = num_occurrences;
1095 }
1096 }
1097
1098 Ok(())
1099 }
1100
1101 fn apply_local_search_post_processing(&mut self) -> Result<()> {
1103 let problem = self.current_problem.as_ref().unwrap();
1104
1105 for solution in &mut self.solutions {
1106 let mut improved_config = solution.configuration.clone();
1107 let mut improved_energy = solution.energy;
1108
1109 for _ in 0..self.config.post_processing.max_local_search_iterations {
1110 let mut found_improvement = false;
1111
1112 for spin in 0..problem.num_spins {
1113 improved_config[spin] *= -1;
1115 let new_energy = problem.calculate_energy(&improved_config);
1116
1117 if new_energy < improved_energy {
1118 improved_energy = new_energy;
1119 found_improvement = true;
1120 break;
1121 } else {
1122 improved_config[spin] *= -1; }
1124 }
1125
1126 if !found_improvement {
1127 break;
1128 }
1129 }
1130
1131 if improved_energy < solution.energy {
1133 solution.configuration = improved_config;
1134 solution.energy = improved_energy;
1135 }
1136 }
1137
1138 Ok(())
1139 }
1140
1141 fn compute_annealing_statistics(&mut self) -> Result<()> {
1143 if self.solutions.is_empty() {
1144 return Ok(());
1145 }
1146
1147 self.stats.num_solutions_found = self.solutions.len();
1148 self.stats.best_energy_found = self
1149 .solutions
1150 .iter()
1151 .map(|s| s.energy)
1152 .fold(f64::INFINITY, f64::min);
1153
1154 if let Some(optimal_energy) = self
1156 .current_problem
1157 .as_ref()
1158 .and_then(|p| p.metadata.optimal_energy)
1159 {
1160 let tolerance = 1e-6;
1161 let successful_solutions = self
1162 .solutions
1163 .iter()
1164 .filter(|s| (s.energy - optimal_energy).abs() < tolerance)
1165 .count();
1166 self.stats.success_probability =
1167 successful_solutions as f64 / self.solutions.len() as f64;
1168 }
1169
1170 Ok(())
1171 }
1172
1173 pub fn get_stats(&self) -> &AnnealingStats {
1175 &self.stats
1176 }
1177
1178 pub fn reset_stats(&mut self) {
1180 self.stats = AnnealingStats::default();
1181 }
1182}
1183
1184#[derive(Debug, Clone)]
1186pub struct AnnealingResult {
1187 pub solutions: Vec<AnnealingSolution>,
1189 pub best_energy: f64,
1191 pub annealing_history: Vec<AnnealingSnapshot>,
1193 pub total_time_ms: f64,
1195 pub success_probability: f64,
1197 pub time_to_solution: TimeToSolutionStats,
1199}
1200
1201pub struct QuantumAnnealingUtils;
1203
1204impl QuantumAnnealingUtils {
1205 pub fn create_max_cut_problem(graph_edges: &[(usize, usize)], weights: &[f64]) -> IsingProblem {
1207 let num_vertices = graph_edges
1208 .iter()
1209 .flat_map(|&(u, v)| [u, v])
1210 .max()
1211 .unwrap_or(0)
1212 + 1;
1213
1214 let mut problem = IsingProblem::new(num_vertices);
1215 problem.metadata.name = Some("Max-Cut".to_string());
1216
1217 for (i, &(u, v)) in graph_edges.iter().enumerate() {
1218 let weight = weights.get(i).copied().unwrap_or(1.0);
1219 problem.set_j(u, v, weight / 2.0);
1222 problem.offset -= weight / 2.0;
1223 }
1224
1225 problem
1226 }
1227
1228 pub fn create_number_partitioning_problem(numbers: &[f64]) -> IsingProblem {
1230 let n = numbers.len();
1231 let mut problem = IsingProblem::new(n);
1232 problem.metadata.name = Some("Number Partitioning".to_string());
1233
1234 for i in 0..n {
1236 problem.offset += numbers[i] * numbers[i];
1237 for j in i + 1..n {
1238 problem.set_j(i, j, 2.0 * numbers[i] * numbers[j]);
1239 }
1240 }
1241
1242 problem
1243 }
1244
1245 pub fn create_random_ising_problem(
1247 num_spins: usize,
1248 h_range: f64,
1249 j_range: f64,
1250 ) -> IsingProblem {
1251 let mut problem = IsingProblem::new(num_spins);
1252 problem.metadata.name = Some("Random Ising".to_string());
1253
1254 for i in 0..num_spins {
1256 problem.set_h(i, (fastrand::f64() - 0.5) * 2.0 * h_range);
1257 }
1258
1259 for i in 0..num_spins {
1261 for j in i + 1..num_spins {
1262 if fastrand::f64() < 0.5 {
1263 problem.set_j(i, j, (fastrand::f64() - 0.5) * 2.0 * j_range);
1265 }
1266 }
1267 }
1268
1269 problem
1270 }
1271
1272 pub fn benchmark_quantum_annealing() -> Result<AnnealingBenchmarkResults> {
1274 let mut results = AnnealingBenchmarkResults::default();
1275
1276 let problem_sizes = vec![8, 12, 16];
1277 let annealing_times = vec![1.0, 10.0, 100.0]; for &size in &problem_sizes {
1280 for &time in &annealing_times {
1281 let problem = Self::create_random_ising_problem(size, 1.0, 1.0);
1283
1284 let config = QuantumAnnealingConfig {
1285 annealing_time: time,
1286 time_steps: (time * 100.0) as usize,
1287 topology: AnnealingTopology::Complete(size),
1288 ..Default::default()
1289 };
1290
1291 let mut simulator = QuantumAnnealingSimulator::new(config)?;
1292 simulator.set_problem(problem)?;
1293
1294 let start = std::time::Instant::now();
1295 let result = simulator.anneal(100)?;
1296 let execution_time = start.elapsed().as_secs_f64() * 1000.0;
1297
1298 results
1299 .execution_times
1300 .push((format!("{}spins_{}us", size, time), execution_time));
1301 results
1302 .best_energies
1303 .push((format!("{}spins_{}us", size, time), result.best_energy));
1304 }
1305 }
1306
1307 Ok(results)
1308 }
1309}
1310
1311#[derive(Debug, Clone, Default)]
1313pub struct AnnealingBenchmarkResults {
1314 pub execution_times: Vec<(String, f64)>,
1316 pub best_energies: Vec<(String, f64)>,
1318}
1319
1320#[cfg(test)]
1321mod tests {
1322 use super::*;
1323 use approx::assert_abs_diff_eq;
1324
1325 #[test]
1326 fn test_ising_problem_creation() {
1327 let mut problem = IsingProblem::new(3);
1328 problem.set_h(0, 0.5);
1329 problem.set_j(0, 1, -1.0);
1330
1331 assert_eq!(problem.num_spins, 3);
1332 assert_eq!(problem.h[0], 0.5);
1333 assert_eq!(problem.j[[0, 1]], -1.0);
1334 assert_eq!(problem.j[[1, 0]], -1.0);
1335 }
1336
1337 #[test]
1338 fn test_ising_energy_calculation() {
1339 let mut problem = IsingProblem::new(2);
1340 problem.set_h(0, 1.0);
1341 problem.set_h(1, -0.5);
1342 problem.set_j(0, 1, 2.0);
1343
1344 let config = vec![1, -1];
1345 let energy = problem.calculate_energy(&config);
1346 assert_abs_diff_eq!(energy, -0.5, epsilon = 1e-10);
1350 }
1351
1352 #[test]
1353 fn test_ising_to_qubo_conversion() {
1354 let mut ising = IsingProblem::new(2);
1355 ising.set_h(0, 1.0);
1356 ising.set_j(0, 1, -1.0);
1357
1358 let qubo = ising.to_qubo();
1359 assert_eq!(qubo.num_variables, 2);
1360
1361 let ising_config = vec![1, -1];
1363 let qubo_config = vec![1, 0]; let ising_energy = ising.calculate_energy(&ising_config);
1366 let qubo_energy = qubo.calculate_energy(&qubo_config);
1367 assert_abs_diff_eq!(ising_energy, qubo_energy, epsilon = 1e-10);
1368 }
1369
1370 #[test]
1371 fn test_quantum_annealing_simulator_creation() {
1372 let config = QuantumAnnealingConfig::default();
1373 let simulator = QuantumAnnealingSimulator::new(config);
1374 assert!(simulator.is_ok());
1375 }
1376
1377 #[test]
1378 fn test_schedule_functions() {
1379 let config = QuantumAnnealingConfig {
1380 annealing_time: 10.0,
1381 schedule_type: AnnealingScheduleType::Linear,
1382 ..Default::default()
1383 };
1384 let simulator = QuantumAnnealingSimulator::new(config).unwrap();
1385
1386 assert_abs_diff_eq!(simulator.schedule_function(0.0), 0.0, epsilon = 1e-10);
1387 assert_abs_diff_eq!(simulator.schedule_function(5.0), 0.5, epsilon = 1e-10);
1388 assert_abs_diff_eq!(simulator.schedule_function(10.0), 1.0, epsilon = 1e-10);
1389 }
1390
1391 #[test]
1392 fn test_max_cut_problem_creation() {
1393 let edges = vec![(0, 1), (1, 2), (2, 0)];
1394 let weights = vec![1.0, 1.0, 1.0];
1395
1396 let problem = QuantumAnnealingUtils::create_max_cut_problem(&edges, &weights);
1397 assert_eq!(problem.num_spins, 3);
1398 assert!(problem.metadata.name.as_ref().unwrap().contains("Max-Cut"));
1399 }
1400
1401 #[test]
1402 fn test_number_partitioning_problem() {
1403 let numbers = vec![3.0, 1.0, 1.0, 2.0, 2.0, 1.0];
1404 let problem = QuantumAnnealingUtils::create_number_partitioning_problem(&numbers);
1405
1406 assert_eq!(problem.num_spins, 6);
1407 assert!(problem
1408 .metadata
1409 .name
1410 .as_ref()
1411 .unwrap()
1412 .contains("Number Partitioning"));
1413 }
1414
1415 #[test]
1416 fn test_small_problem_annealing() {
1417 let problem = QuantumAnnealingUtils::create_random_ising_problem(3, 1.0, 1.0);
1418
1419 let config = QuantumAnnealingConfig {
1420 annealing_time: 1.0,
1421 time_steps: 100,
1422 topology: AnnealingTopology::Complete(3),
1423 enable_noise: false, ..Default::default()
1425 };
1426
1427 let mut simulator = QuantumAnnealingSimulator::new(config).unwrap();
1428 simulator.set_problem(problem).unwrap();
1429
1430 let result = simulator.anneal(10);
1431 assert!(result.is_ok());
1432
1433 let annealing_result = result.unwrap();
1434 assert_eq!(annealing_result.solutions.len(), 10);
1435 assert!(!annealing_result.annealing_history.is_empty());
1436 }
1437
1438 #[test]
1439 fn test_field_strength_calculation() {
1440 let config = QuantumAnnealingConfig::default();
1441 let simulator = QuantumAnnealingSimulator::new(config).unwrap();
1442
1443 let (transverse, longitudinal) = simulator.calculate_field_strengths(0.0);
1444 assert_abs_diff_eq!(transverse, 1.0, epsilon = 1e-10);
1445 assert_abs_diff_eq!(longitudinal, 0.0, epsilon = 1e-10);
1446
1447 let (transverse, longitudinal) = simulator.calculate_field_strengths(1.0);
1448 assert_abs_diff_eq!(transverse, 0.0, epsilon = 1e-10);
1449 assert_abs_diff_eq!(longitudinal, 1.0, epsilon = 1e-10);
1450 }
1451
1452 #[test]
1453 fn test_annealing_topologies() {
1454 let topologies = vec![
1455 AnnealingTopology::Chimera(4),
1456 AnnealingTopology::Pegasus(3),
1457 AnnealingTopology::Complete(5),
1458 ];
1459
1460 for topology in topologies {
1461 let config = QuantumAnnealingConfig {
1462 topology,
1463 ..Default::default()
1464 };
1465 let simulator = QuantumAnnealingSimulator::new(config);
1466 assert!(simulator.is_ok());
1467 }
1468 }
1469
1470 #[test]
1471 fn test_ising_ground_state_brute_force() {
1472 let mut problem = IsingProblem::new(2);
1474 problem.set_j(0, 1, -1.0); let (ground_state, ground_energy) = problem.find_ground_state_brute_force();
1477
1478 assert!(ground_state == vec![1, 1] || ground_state == vec![-1, -1]);
1480 assert_abs_diff_eq!(ground_energy, -1.0, epsilon = 1e-10);
1481 }
1482
1483 #[test]
1484 fn test_post_processing_config() {
1485 let config = PostProcessingConfig::default();
1486 assert!(config.enable_spin_reversal);
1487 assert!(config.enable_local_search);
1488 assert!(config.enable_majority_vote);
1489 assert_eq!(config.majority_vote_reads, 1000);
1490 }
1491}