1use crate::prelude::SimulatorError;
10use scirs2_core::ndarray::{Array1, Array2};
11use scirs2_core::parallel_ops::{IndexedParallelIterator, ParallelIterator};
12use scirs2_core::Complex64;
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 #[must_use]
197 pub fn new(num_spins: usize) -> Self {
198 Self {
199 num_spins,
200 h: Array1::zeros(num_spins),
201 j: Array2::zeros((num_spins, num_spins)),
202 offset: 0.0,
203 metadata: ProblemMetadata::default(),
204 }
205 }
206
207 pub fn set_h(&mut self, i: usize, value: f64) {
209 if i < self.num_spins {
210 self.h[i] = value;
211 }
212 }
213
214 pub fn set_j(&mut self, i: usize, j: usize, value: f64) {
216 if i < self.num_spins && j < self.num_spins {
217 self.j[[i, j]] = value;
218 self.j[[j, i]] = value; }
220 }
221
222 #[must_use]
224 pub fn calculate_energy(&self, configuration: &[i8]) -> f64 {
225 if configuration.len() != self.num_spins {
226 return f64::INFINITY;
227 }
228
229 let mut energy = self.offset;
230
231 for i in 0..self.num_spins {
233 energy += self.h[i] * f64::from(configuration[i]);
234 }
235
236 for i in 0..self.num_spins {
238 for j in i + 1..self.num_spins {
239 energy +=
240 self.j[[i, j]] * f64::from(configuration[i]) * f64::from(configuration[j]);
241 }
242 }
243
244 energy
245 }
246
247 #[must_use]
249 pub fn to_qubo(&self) -> QUBOProblem {
250 let num_vars = self.num_spins;
251 let mut q = Array2::zeros((num_vars, num_vars));
252 let mut linear = Array1::zeros(num_vars);
253 let mut offset = self.offset;
254
255 for i in 0..num_vars {
260 linear[i] += 2.0 * self.h[i];
262 offset -= self.h[i];
263
264 for j in i + 1..num_vars {
265 q[[i, j]] += 4.0 * self.j[[i, j]];
268 linear[i] -= 2.0 * self.j[[i, j]];
269 linear[j] -= 2.0 * self.j[[i, j]];
270 offset += self.j[[i, j]];
271 }
272 }
273
274 QUBOProblem {
275 num_variables: num_vars,
276 q,
277 linear,
278 offset,
279 metadata: self.metadata.clone(),
280 }
281 }
282
283 #[must_use]
285 pub fn find_ground_state_brute_force(&self) -> (Vec<i8>, f64) {
286 assert!(
287 (self.num_spins <= 20),
288 "Brute force search only supported for <= 20 spins"
289 );
290
291 let mut best_config = vec![-1; self.num_spins];
292 let mut best_energy = f64::INFINITY;
293
294 for state in 0..(1 << self.num_spins) {
295 let mut config = vec![-1; self.num_spins];
296 for i in 0..self.num_spins {
297 if (state >> i) & 1 == 1 {
298 config[i] = 1;
299 }
300 }
301
302 let energy = self.calculate_energy(&config);
303 if energy < best_energy {
304 best_energy = energy;
305 best_config = config;
306 }
307 }
308
309 (best_config, best_energy)
310 }
311}
312
313impl QUBOProblem {
314 #[must_use]
316 pub fn new(num_variables: usize) -> Self {
317 Self {
318 num_variables,
319 q: Array2::zeros((num_variables, num_variables)),
320 linear: Array1::zeros(num_variables),
321 offset: 0.0,
322 metadata: ProblemMetadata::default(),
323 }
324 }
325
326 #[must_use]
328 pub fn calculate_energy(&self, configuration: &[u8]) -> f64 {
329 if configuration.len() != self.num_variables {
330 return f64::INFINITY;
331 }
332
333 let mut energy = self.offset;
334
335 for i in 0..self.num_variables {
337 energy += self.linear[i] * f64::from(configuration[i]);
338 }
339
340 for i in 0..self.num_variables {
342 for j in 0..self.num_variables {
343 if i != j {
344 energy +=
345 self.q[[i, j]] * f64::from(configuration[i]) * f64::from(configuration[j]);
346 }
347 }
348 }
349
350 energy
351 }
352
353 #[must_use]
355 pub fn to_ising(&self) -> IsingProblem {
356 let num_spins = self.num_variables;
357 let mut h = Array1::zeros(num_spins);
358 let mut j = Array2::zeros((num_spins, num_spins));
359 let mut offset = self.offset;
360
361 for i in 0..num_spins {
363 h[i] = self.linear[i] / 2.0;
364 offset += self.linear[i] / 2.0;
365
366 for k in 0..num_spins {
367 if k != i {
368 h[i] += self.q[[i, k]] / 4.0;
369 offset += self.q[[i, k]] / 4.0;
370 }
371 }
372 }
373
374 for i in 0..num_spins {
375 for k in i + 1..num_spins {
376 j[[i, k]] = self.q[[i, k]] / 4.0;
377 }
378 }
379
380 IsingProblem {
381 num_spins,
382 h,
383 j,
384 offset,
385 metadata: self.metadata.clone(),
386 }
387 }
388}
389
390pub struct QuantumAnnealingSimulator {
392 config: QuantumAnnealingConfig,
394 current_problem: Option<IsingProblem>,
396 noise_simulator: Option<DeviceNoiseSimulator>,
398 backend: Option<SciRS2Backend>,
400 annealing_history: Vec<AnnealingSnapshot>,
402 solutions: Vec<AnnealingSolution>,
404 stats: AnnealingStats,
406}
407
408#[derive(Debug, Clone)]
410pub struct AnnealingSnapshot {
411 pub time: f64,
413 pub s: f64,
415 pub transverse_field: f64,
417 pub longitudinal_field: f64,
419 pub quantum_state: Option<Array1<Complex64>>,
421 pub classical_probabilities: Option<Array1<f64>>,
423 pub energy_expectation: f64,
425 pub temperature_factor: f64,
427}
428
429#[derive(Debug, Clone)]
431pub struct AnnealingSolution {
432 pub configuration: Vec<i8>,
434 pub energy: f64,
436 pub probability: f64,
438 pub num_occurrences: usize,
440 pub rank: usize,
442}
443
444#[derive(Debug, Clone, Default, Serialize, Deserialize)]
446pub struct AnnealingStats {
447 pub total_annealing_time_ms: f64,
449 pub num_annealing_runs: usize,
451 pub num_solutions_found: usize,
453 pub best_energy_found: f64,
455 pub success_probability: f64,
457 pub time_to_solution: TimeToSolutionStats,
459 pub noise_stats: NoiseStats,
461}
462
463#[derive(Debug, Clone, Default, Serialize, Deserialize)]
465pub struct TimeToSolutionStats {
466 pub median_tts: f64,
468 pub percentile_99_tts: f64,
470 pub success_rate: f64,
472}
473
474#[derive(Debug, Clone, Default, Serialize, Deserialize)]
476pub struct NoiseStats {
477 pub thermal_excitations: usize,
479 pub control_errors: usize,
481 pub decoherence_events: usize,
483}
484
485impl QuantumAnnealingSimulator {
486 pub fn new(config: QuantumAnnealingConfig) -> Result<Self> {
488 Ok(Self {
489 config,
490 current_problem: None,
491 noise_simulator: None,
492 backend: None,
493 annealing_history: Vec::new(),
494 solutions: Vec::new(),
495 stats: AnnealingStats::default(),
496 })
497 }
498
499 pub fn with_backend(mut self) -> Result<Self> {
501 self.backend = Some(SciRS2Backend::new());
502 Ok(self)
503 }
504
505 pub fn set_problem(&mut self, problem: IsingProblem) -> Result<()> {
507 let max_spins = match &self.config.topology {
509 AnnealingTopology::Chimera(size) => size * size * 8,
510 AnnealingTopology::Pegasus(size) => size * (size - 1) * 12,
511 AnnealingTopology::Zephyr(size) => size * size * 8,
512 AnnealingTopology::Complete(size) => *size,
513 AnnealingTopology::Custom(topology) => topology.num_qubits,
514 };
515
516 if problem.num_spins > max_spins {
517 return Err(SimulatorError::InvalidInput(format!(
518 "Problem size {} exceeds topology limit {}",
519 problem.num_spins, max_spins
520 )));
521 }
522
523 self.current_problem = Some(problem);
524 Ok(())
525 }
526
527 pub fn anneal(&mut self, num_reads: usize) -> Result<AnnealingResult> {
529 let problem = self
530 .current_problem
531 .as_ref()
532 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
533
534 let start_time = std::time::Instant::now();
535 self.solutions.clear();
536
537 for read in 0..num_reads {
538 let read_start = std::time::Instant::now();
539
540 let solution = self.single_anneal(read)?;
542 self.solutions.push(solution);
543
544 if read % 100 == 0 {
545 println!(
546 "Completed read {}/{}, time={:.2}ms",
547 read,
548 num_reads,
549 read_start.elapsed().as_secs_f64() * 1000.0
550 );
551 }
552 }
553
554 if self.config.post_processing.enable_majority_vote {
556 self.apply_majority_vote_post_processing()?;
557 }
558
559 if self.config.post_processing.enable_local_search {
560 self.apply_local_search_post_processing()?;
561 }
562
563 self.solutions.sort_by(|a, b| {
565 a.energy
566 .partial_cmp(&b.energy)
567 .unwrap_or(std::cmp::Ordering::Equal)
568 });
569
570 for (rank, solution) in self.solutions.iter_mut().enumerate() {
572 solution.rank = rank;
573 }
574
575 self.compute_annealing_statistics()?;
577
578 let total_time = start_time.elapsed().as_secs_f64() * 1000.0;
579 self.stats.total_annealing_time_ms += total_time;
580 self.stats.num_annealing_runs += num_reads;
581
582 Ok(AnnealingResult {
583 solutions: self.solutions.clone(),
584 best_energy: self.solutions.first().map_or(f64::INFINITY, |s| s.energy),
585 annealing_history: self.annealing_history.clone(),
586 total_time_ms: total_time,
587 success_probability: self.stats.success_probability,
588 time_to_solution: self.stats.time_to_solution.clone(),
589 })
590 }
591
592 fn single_anneal(&mut self, _read_id: usize) -> Result<AnnealingSolution> {
594 let problem = self
595 .current_problem
596 .as_ref()
597 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
598 let problem_num_spins = problem.num_spins;
599
600 let state_size = 1 << problem_num_spins.min(20); let mut quantum_state = if problem_num_spins <= 20 {
603 let mut state = Array1::zeros(state_size);
604 let amplitude = (1.0 / state_size as f64).sqrt();
606 state.fill(Complex64::new(amplitude, 0.0));
607 Some(state)
608 } else {
609 None };
611
612 let dt = self.config.annealing_time / self.config.time_steps as f64;
613 self.annealing_history.clear();
614
615 for step in 0..=self.config.time_steps {
617 let t = step as f64 * dt;
618 let s = self.schedule_function(t);
619
620 let (transverse_field, longitudinal_field) = self.calculate_field_strengths(s);
622
623 if let Some(ref mut state) = quantum_state {
625 self.apply_quantum_evolution(state, transverse_field, longitudinal_field, dt)?;
626
627 if self.config.enable_noise {
629 self.apply_annealing_noise(state, dt)?;
630 }
631 }
632
633 if step % (self.config.time_steps / 100) == 0 {
635 let snapshot = self.take_annealing_snapshot(
636 t,
637 s,
638 transverse_field,
639 longitudinal_field,
640 quantum_state.as_ref(),
641 )?;
642 self.annealing_history.push(snapshot);
643 }
644 }
645
646 let final_configuration = if let Some(ref state) = quantum_state {
648 self.measure_final_state(state)?
649 } else {
650 let problem = self
652 .current_problem
653 .as_ref()
654 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
655 self.classical_sampling(problem)?
656 };
657
658 let energy = self
659 .current_problem
660 .as_ref()
661 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?
662 .calculate_energy(&final_configuration);
663
664 Ok(AnnealingSolution {
665 configuration: final_configuration,
666 energy,
667 probability: 1.0 / (self.config.time_steps as f64), num_occurrences: 1,
669 rank: 0,
670 })
671 }
672
673 fn schedule_function(&self, t: f64) -> f64 {
675 let normalized_t = t / self.config.annealing_time;
676
677 match self.config.schedule_type {
678 AnnealingScheduleType::Linear => normalized_t,
679 AnnealingScheduleType::DWave => {
680 if normalized_t < 0.1 {
682 5.0 * normalized_t * normalized_t
683 } else if normalized_t < 0.9 {
684 0.05 + 0.9 * (normalized_t - 0.1) / 0.8
685 } else {
686 0.05f64.mul_add(
687 1.0 - (1.0 - normalized_t) * (1.0 - normalized_t) / 0.01,
688 0.95,
689 )
690 }
691 }
692 AnnealingScheduleType::Optimized => {
693 self.optimized_schedule(normalized_t)
695 }
696 AnnealingScheduleType::CustomPause {
697 pause_start,
698 pause_duration,
699 } => {
700 if normalized_t >= pause_start && normalized_t <= pause_start + pause_duration {
701 pause_start } else if normalized_t > pause_start + pause_duration {
703 (normalized_t - pause_duration - pause_start) / (1.0 - pause_duration)
704 } else {
705 normalized_t / pause_start
706 }
707 }
708 AnnealingScheduleType::NonMonotonic => {
709 (0.1 * (10.0 * std::f64::consts::PI * normalized_t).sin())
711 .mul_add(1.0 - normalized_t, normalized_t)
712 }
713 AnnealingScheduleType::Reverse { reinitialize_point } => {
714 if normalized_t < reinitialize_point {
715 1.0 } else {
717 1.0 - (normalized_t - reinitialize_point) / (1.0 - reinitialize_point)
718 }
719 }
720 }
721 }
722
723 fn optimized_schedule(&self, t: f64) -> f64 {
725 if t < 0.3 {
728 t * t / 0.09 * 0.3
729 } else if t < 0.7 {
730 0.3 + (t - 0.3) * 0.4 / 0.4
731 } else {
732 ((t - 0.7) * (t - 0.7) / 0.09).mul_add(0.3, 0.7)
733 }
734 }
735
736 fn calculate_field_strengths(&self, s: f64) -> (f64, f64) {
738 let a_s = (1.0 - s) * 1.0; let b_s = s * 1.0; (a_s, b_s)
742 }
743
744 fn apply_quantum_evolution(
746 &self,
747 state: &mut Array1<Complex64>,
748 transverse_field: f64,
749 longitudinal_field: f64,
750 dt: f64,
751 ) -> Result<()> {
752 let _problem = self
754 .current_problem
755 .as_ref()
756 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
757
758 let hamiltonian = self.build_annealing_hamiltonian(transverse_field, longitudinal_field)?;
760
761 let evolution_operator = self.compute_evolution_operator(&hamiltonian, dt)?;
763 *state = evolution_operator.dot(state);
764
765 let norm: f64 = state
767 .iter()
768 .map(scirs2_core::Complex::norm_sqr)
769 .sum::<f64>()
770 .sqrt();
771 if norm > 1e-15 {
772 state.mapv_inplace(|x| x / norm);
773 }
774
775 Ok(())
776 }
777
778 fn build_annealing_hamiltonian(
780 &self,
781 transverse_field: f64,
782 longitudinal_field: f64,
783 ) -> Result<Array2<Complex64>> {
784 let problem = self
785 .current_problem
786 .as_ref()
787 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
788 let num_spins = problem.num_spins;
789 let dim = 1 << num_spins;
790 let mut hamiltonian = Array2::zeros((dim, dim));
791
792 for spin in 0..num_spins {
794 let sigma_x = self.build_sigma_x(spin, num_spins);
795 hamiltonian = hamiltonian - sigma_x.mapv(|x| x * transverse_field);
796 }
797
798 let problem_hamiltonian = self.build_problem_hamiltonian()?;
800 hamiltonian = hamiltonian + problem_hamiltonian.mapv(|x| x * longitudinal_field);
801
802 Ok(hamiltonian)
803 }
804
805 fn build_sigma_x(&self, target_spin: usize, num_spins: usize) -> Array2<Complex64> {
807 let dim = 1 << num_spins;
808 let mut sigma_x = Array2::zeros((dim, dim));
809
810 for i in 0..dim {
811 let j = i ^ (1 << target_spin); sigma_x[[i, j]] = Complex64::new(1.0, 0.0);
813 }
814
815 sigma_x
816 }
817
818 fn build_problem_hamiltonian(&self) -> Result<Array2<Complex64>> {
820 let problem = self
821 .current_problem
822 .as_ref()
823 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
824 let num_spins = problem.num_spins;
825 let dim = 1 << num_spins;
826 let mut hamiltonian = Array2::zeros((dim, dim));
827
828 for i in 0..num_spins {
830 let sigma_z = self.build_sigma_z(i, num_spins);
831 hamiltonian = hamiltonian + sigma_z.mapv(|x| x * problem.h[i]);
832 }
833
834 for i in 0..num_spins {
836 for j in i + 1..num_spins {
837 if problem.j[[i, j]] != 0.0 {
838 let sigma_z_i = self.build_sigma_z(i, num_spins);
839 let sigma_z_j = self.build_sigma_z(j, num_spins);
840 let sigma_z_ij = sigma_z_i.dot(&sigma_z_j);
841 hamiltonian = hamiltonian + sigma_z_ij.mapv(|x| x * problem.j[[i, j]]);
842 }
843 }
844 }
845
846 for i in 0..dim {
848 hamiltonian[[i, i]] += Complex64::new(problem.offset, 0.0);
849 }
850
851 Ok(hamiltonian)
852 }
853
854 fn build_sigma_z(&self, target_spin: usize, num_spins: usize) -> Array2<Complex64> {
856 let dim = 1 << num_spins;
857 let mut sigma_z = Array2::zeros((dim, dim));
858
859 for i in 0..dim {
860 let sign = if (i >> target_spin) & 1 == 0 {
861 1.0
862 } else {
863 -1.0
864 };
865 sigma_z[[i, i]] = Complex64::new(sign, 0.0);
866 }
867
868 sigma_z
869 }
870
871 fn compute_evolution_operator(
873 &self,
874 hamiltonian: &Array2<Complex64>,
875 dt: f64,
876 ) -> Result<Array2<Complex64>> {
877 self.matrix_exponential(hamiltonian, -Complex64::new(0.0, dt))
879 }
880
881 fn matrix_exponential(
883 &self,
884 matrix: &Array2<Complex64>,
885 factor: Complex64,
886 ) -> Result<Array2<Complex64>> {
887 let dim = matrix.dim().0;
888 let scaled_matrix = matrix.mapv(|x| x * factor);
889
890 let mut result = Array2::eye(dim);
891 let mut term = Array2::eye(dim);
892
893 for n in 1..=15 {
894 term = term.dot(&scaled_matrix) / f64::from(n);
896 let term_norm: f64 = term
897 .iter()
898 .map(scirs2_core::Complex::norm_sqr)
899 .sum::<f64>()
900 .sqrt();
901
902 result += &term;
903
904 if term_norm < 1e-12 {
905 break;
906 }
907 }
908
909 Ok(result)
910 }
911
912 fn apply_annealing_noise(&mut self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
914 if self.config.enable_thermal_fluctuations {
915 self.apply_thermal_noise(state, dt)?;
916 self.stats.noise_stats.thermal_excitations += 1;
917 }
918
919 if self.config.enable_control_errors {
920 self.apply_control_error_noise(state, dt)?;
921 self.stats.noise_stats.control_errors += 1;
922 }
923
924 self.apply_decoherence_noise(state, dt)?;
926 self.stats.noise_stats.decoherence_events += 1;
927
928 Ok(())
929 }
930
931 fn apply_thermal_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
933 let kb_t = 1.38e-23 * self.config.temperature; let thermal_energy = kb_t * dt * 1e6; for amplitude in state.iter_mut() {
938 let thermal_phase = fastrand::f64() * thermal_energy * 2.0 * std::f64::consts::PI;
939 *amplitude *= Complex64::new(0.0, thermal_phase).exp();
940 }
941
942 Ok(())
943 }
944
945 fn apply_control_error_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
947 let error_strength = 0.01; let problem = self
953 .current_problem
954 .as_ref()
955 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
956 for spin in 0..problem.num_spins.min(10) {
957 if fastrand::f64() < error_strength * dt {
959 let error_angle = fastrand::f64() * 0.1; self.apply_single_spin_rotation(state, spin, error_angle)?;
961 }
962 }
963
964 Ok(())
965 }
966
967 fn apply_decoherence_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
969 let decoherence_rate = 1e-3; let decoherence_prob = decoherence_rate * dt;
971
972 for amplitude in state.iter_mut() {
973 if fastrand::f64() < decoherence_prob {
974 let phase = fastrand::f64() * 2.0 * std::f64::consts::PI;
976 *amplitude *= Complex64::new(0.0, phase).exp();
977 }
978 }
979
980 Ok(())
981 }
982
983 fn apply_single_spin_rotation(
985 &self,
986 state: &mut Array1<Complex64>,
987 spin: usize,
988 angle: f64,
989 ) -> Result<()> {
990 let _problem = self
992 .current_problem
993 .as_ref()
994 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
995 let spin_mask = 1 << spin;
996 let cos_half = (angle / 2.0).cos();
997 let sin_half = (angle / 2.0).sin();
998
999 for i in 0..state.len() {
1000 if i & spin_mask == 0 {
1001 let j = i | spin_mask;
1002 if j < state.len() {
1003 let amp_0 = state[i];
1004 let amp_1 = state[j];
1005
1006 state[i] = cos_half * amp_0 - Complex64::new(0.0, sin_half) * amp_1;
1007 state[j] = cos_half * amp_1 - Complex64::new(0.0, sin_half) * amp_0;
1008 }
1009 }
1010 }
1011
1012 Ok(())
1013 }
1014
1015 fn take_annealing_snapshot(
1017 &self,
1018 time: f64,
1019 s: f64,
1020 transverse_field: f64,
1021 longitudinal_field: f64,
1022 quantum_state: Option<&Array1<Complex64>>,
1023 ) -> Result<AnnealingSnapshot> {
1024 let energy_expectation = if let Some(state) = quantum_state {
1025 self.calculate_energy_expectation(state)?
1026 } else {
1027 0.0
1028 };
1029
1030 let temperature_factor = (-1.0 / (1.38e-23 * self.config.temperature)).exp();
1031
1032 Ok(AnnealingSnapshot {
1033 time,
1034 s,
1035 transverse_field,
1036 longitudinal_field,
1037 quantum_state: quantum_state.cloned(),
1038 classical_probabilities: None,
1039 energy_expectation,
1040 temperature_factor,
1041 })
1042 }
1043
1044 fn calculate_energy_expectation(&self, state: &Array1<Complex64>) -> Result<f64> {
1046 let problem = self
1047 .current_problem
1048 .as_ref()
1049 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
1050 let mut expectation = 0.0;
1051
1052 for (i, &litude) in state.iter().enumerate() {
1053 let prob = amplitude.norm_sqr();
1054
1055 let mut config = vec![-1; problem.num_spins];
1057 for spin in 0..problem.num_spins {
1058 if (i >> spin) & 1 == 1 {
1059 config[spin] = 1;
1060 }
1061 }
1062
1063 let energy = problem.calculate_energy(&config);
1064 expectation += prob * energy;
1065 }
1066
1067 Ok(expectation)
1068 }
1069
1070 fn measure_final_state(&self, state: &Array1<Complex64>) -> Result<Vec<i8>> {
1072 let problem = self
1073 .current_problem
1074 .as_ref()
1075 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
1076
1077 let probabilities: Vec<f64> = state.iter().map(scirs2_core::Complex::norm_sqr).collect();
1079 let random_val = fastrand::f64();
1080
1081 let mut cumulative_prob = 0.0;
1082 for (i, &prob) in probabilities.iter().enumerate() {
1083 cumulative_prob += prob;
1084 if random_val < cumulative_prob {
1085 let mut config = vec![-1; problem.num_spins];
1087 for spin in 0..problem.num_spins {
1088 if (i >> spin) & 1 == 1 {
1089 config[spin] = 1;
1090 }
1091 }
1092 return Ok(config);
1093 }
1094 }
1095
1096 Ok(vec![-1; problem.num_spins])
1098 }
1099
1100 fn classical_sampling(&self, problem: &IsingProblem) -> Result<Vec<i8>> {
1102 let mut config: Vec<i8> = (0..problem.num_spins)
1104 .map(|_| if fastrand::f64() > 0.5 { 1 } else { -1 })
1105 .collect();
1106
1107 for _ in 0..1000 {
1109 let spin_to_flip = fastrand::usize(0..problem.num_spins);
1110 let old_energy = problem.calculate_energy(&config);
1111
1112 config[spin_to_flip] *= -1;
1113 let new_energy = problem.calculate_energy(&config);
1114
1115 if new_energy > old_energy {
1116 config[spin_to_flip] *= -1; }
1118 }
1119
1120 Ok(config)
1121 }
1122
1123 fn apply_majority_vote_post_processing(&mut self) -> Result<()> {
1125 if self.solutions.is_empty() {
1126 return Ok(());
1127 }
1128
1129 let mut config_groups: HashMap<Vec<i8>, Vec<usize>> = HashMap::new();
1131 for (i, solution) in self.solutions.iter().enumerate() {
1132 config_groups
1133 .entry(solution.configuration.clone())
1134 .or_default()
1135 .push(i);
1136 }
1137
1138 for (config, indices) in config_groups {
1140 let num_occurrences = indices.len();
1141 for &idx in &indices {
1142 self.solutions[idx].num_occurrences = num_occurrences;
1143 }
1144 }
1145
1146 Ok(())
1147 }
1148
1149 fn apply_local_search_post_processing(&mut self) -> Result<()> {
1151 let problem = self
1152 .current_problem
1153 .as_ref()
1154 .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
1155
1156 for solution in &mut self.solutions {
1157 let mut improved_config = solution.configuration.clone();
1158 let mut improved_energy = solution.energy;
1159
1160 for _ in 0..self.config.post_processing.max_local_search_iterations {
1161 let mut found_improvement = false;
1162
1163 for spin in 0..problem.num_spins {
1164 improved_config[spin] *= -1;
1166 let new_energy = problem.calculate_energy(&improved_config);
1167
1168 if new_energy < improved_energy {
1169 improved_energy = new_energy;
1170 found_improvement = true;
1171 break;
1172 }
1173 improved_config[spin] *= -1; }
1175
1176 if !found_improvement {
1177 break;
1178 }
1179 }
1180
1181 if improved_energy < solution.energy {
1183 solution.configuration = improved_config;
1184 solution.energy = improved_energy;
1185 }
1186 }
1187
1188 Ok(())
1189 }
1190
1191 fn compute_annealing_statistics(&mut self) -> Result<()> {
1193 if self.solutions.is_empty() {
1194 return Ok(());
1195 }
1196
1197 self.stats.num_solutions_found = self.solutions.len();
1198 self.stats.best_energy_found = self
1199 .solutions
1200 .iter()
1201 .map(|s| s.energy)
1202 .fold(f64::INFINITY, f64::min);
1203
1204 if let Some(optimal_energy) = self
1206 .current_problem
1207 .as_ref()
1208 .and_then(|p| p.metadata.optimal_energy)
1209 {
1210 let tolerance = 1e-6;
1211 let successful_solutions = self
1212 .solutions
1213 .iter()
1214 .filter(|s| (s.energy - optimal_energy).abs() < tolerance)
1215 .count();
1216 self.stats.success_probability =
1217 successful_solutions as f64 / self.solutions.len() as f64;
1218 }
1219
1220 Ok(())
1221 }
1222
1223 #[must_use]
1225 pub const fn get_stats(&self) -> &AnnealingStats {
1226 &self.stats
1227 }
1228
1229 pub fn reset_stats(&mut self) {
1231 self.stats = AnnealingStats::default();
1232 }
1233}
1234
1235#[derive(Debug, Clone)]
1237pub struct AnnealingResult {
1238 pub solutions: Vec<AnnealingSolution>,
1240 pub best_energy: f64,
1242 pub annealing_history: Vec<AnnealingSnapshot>,
1244 pub total_time_ms: f64,
1246 pub success_probability: f64,
1248 pub time_to_solution: TimeToSolutionStats,
1250}
1251
1252pub struct QuantumAnnealingUtils;
1254
1255impl QuantumAnnealingUtils {
1256 #[must_use]
1258 pub fn create_max_cut_problem(graph_edges: &[(usize, usize)], weights: &[f64]) -> IsingProblem {
1259 let num_vertices = graph_edges
1260 .iter()
1261 .flat_map(|&(u, v)| [u, v])
1262 .max()
1263 .unwrap_or(0)
1264 + 1;
1265
1266 let mut problem = IsingProblem::new(num_vertices);
1267 problem.metadata.name = Some("Max-Cut".to_string());
1268
1269 for (i, &(u, v)) in graph_edges.iter().enumerate() {
1270 let weight = weights.get(i).copied().unwrap_or(1.0);
1271 problem.set_j(u, v, weight / 2.0);
1274 problem.offset -= weight / 2.0;
1275 }
1276
1277 problem
1278 }
1279
1280 #[must_use]
1282 pub fn create_number_partitioning_problem(numbers: &[f64]) -> IsingProblem {
1283 let n = numbers.len();
1284 let mut problem = IsingProblem::new(n);
1285 problem.metadata.name = Some("Number Partitioning".to_string());
1286
1287 for i in 0..n {
1289 problem.offset += numbers[i] * numbers[i];
1290 for j in i + 1..n {
1291 problem.set_j(i, j, 2.0 * numbers[i] * numbers[j]);
1292 }
1293 }
1294
1295 problem
1296 }
1297
1298 #[must_use]
1300 pub fn create_random_ising_problem(
1301 num_spins: usize,
1302 h_range: f64,
1303 j_range: f64,
1304 ) -> IsingProblem {
1305 let mut problem = IsingProblem::new(num_spins);
1306 problem.metadata.name = Some("Random Ising".to_string());
1307
1308 for i in 0..num_spins {
1310 problem.set_h(i, (fastrand::f64() - 0.5) * 2.0 * h_range);
1311 }
1312
1313 for i in 0..num_spins {
1315 for j in i + 1..num_spins {
1316 if fastrand::f64() < 0.5 {
1317 problem.set_j(i, j, (fastrand::f64() - 0.5) * 2.0 * j_range);
1319 }
1320 }
1321 }
1322
1323 problem
1324 }
1325
1326 pub fn benchmark_quantum_annealing() -> Result<AnnealingBenchmarkResults> {
1328 let mut results = AnnealingBenchmarkResults::default();
1329
1330 let problem_sizes = vec![8, 12, 16];
1331 let annealing_times = vec![1.0, 10.0, 100.0]; for &size in &problem_sizes {
1334 for &time in &annealing_times {
1335 let problem = Self::create_random_ising_problem(size, 1.0, 1.0);
1337
1338 let config = QuantumAnnealingConfig {
1339 annealing_time: time,
1340 time_steps: (time * 100.0) as usize,
1341 topology: AnnealingTopology::Complete(size),
1342 ..Default::default()
1343 };
1344
1345 let mut simulator = QuantumAnnealingSimulator::new(config)?;
1346 simulator.set_problem(problem)?;
1347
1348 let start = std::time::Instant::now();
1349 let result = simulator.anneal(100)?;
1350 let execution_time = start.elapsed().as_secs_f64() * 1000.0;
1351
1352 results
1353 .execution_times
1354 .push((format!("{size}spins_{time}us"), execution_time));
1355 results
1356 .best_energies
1357 .push((format!("{size}spins_{time}us"), result.best_energy));
1358 }
1359 }
1360
1361 Ok(results)
1362 }
1363}
1364
1365#[derive(Debug, Clone, Default)]
1367pub struct AnnealingBenchmarkResults {
1368 pub execution_times: Vec<(String, f64)>,
1370 pub best_energies: Vec<(String, f64)>,
1372}
1373
1374#[cfg(test)]
1375mod tests {
1376 use super::*;
1377 use approx::assert_abs_diff_eq;
1378
1379 #[test]
1380 fn test_ising_problem_creation() {
1381 let mut problem = IsingProblem::new(3);
1382 problem.set_h(0, 0.5);
1383 problem.set_j(0, 1, -1.0);
1384
1385 assert_eq!(problem.num_spins, 3);
1386 assert_eq!(problem.h[0], 0.5);
1387 assert_eq!(problem.j[[0, 1]], -1.0);
1388 assert_eq!(problem.j[[1, 0]], -1.0);
1389 }
1390
1391 #[test]
1392 fn test_ising_energy_calculation() {
1393 let mut problem = IsingProblem::new(2);
1394 problem.set_h(0, 1.0);
1395 problem.set_h(1, -0.5);
1396 problem.set_j(0, 1, 2.0);
1397
1398 let config = vec![1, -1];
1399 let energy = problem.calculate_energy(&config);
1400 assert_abs_diff_eq!(energy, -0.5, epsilon = 1e-10);
1404 }
1405
1406 #[test]
1407 fn test_ising_to_qubo_conversion() {
1408 let mut ising = IsingProblem::new(2);
1409 ising.set_h(0, 1.0);
1410 ising.set_j(0, 1, -1.0);
1411
1412 let qubo = ising.to_qubo();
1413 assert_eq!(qubo.num_variables, 2);
1414
1415 let ising_config = vec![1, -1];
1417 let qubo_config = vec![1, 0]; let ising_energy = ising.calculate_energy(&ising_config);
1420 let qubo_energy = qubo.calculate_energy(&qubo_config);
1421 assert_abs_diff_eq!(ising_energy, qubo_energy, epsilon = 1e-10);
1422 }
1423
1424 #[test]
1425 fn test_quantum_annealing_simulator_creation() {
1426 let config = QuantumAnnealingConfig::default();
1427 let simulator = QuantumAnnealingSimulator::new(config);
1428 assert!(simulator.is_ok());
1429 }
1430
1431 #[test]
1432 fn test_schedule_functions() {
1433 let config = QuantumAnnealingConfig {
1434 annealing_time: 10.0,
1435 schedule_type: AnnealingScheduleType::Linear,
1436 ..Default::default()
1437 };
1438 let simulator = QuantumAnnealingSimulator::new(config)
1439 .expect("should create quantum annealing simulator");
1440
1441 assert_abs_diff_eq!(simulator.schedule_function(0.0), 0.0, epsilon = 1e-10);
1442 assert_abs_diff_eq!(simulator.schedule_function(5.0), 0.5, epsilon = 1e-10);
1443 assert_abs_diff_eq!(simulator.schedule_function(10.0), 1.0, epsilon = 1e-10);
1444 }
1445
1446 #[test]
1447 fn test_max_cut_problem_creation() {
1448 let edges = vec![(0, 1), (1, 2), (2, 0)];
1449 let weights = vec![1.0, 1.0, 1.0];
1450
1451 let problem = QuantumAnnealingUtils::create_max_cut_problem(&edges, &weights);
1452 assert_eq!(problem.num_spins, 3);
1453 assert!(problem
1454 .metadata
1455 .name
1456 .as_ref()
1457 .expect("metadata name should be set")
1458 .contains("Max-Cut"));
1459 }
1460
1461 #[test]
1462 fn test_number_partitioning_problem() {
1463 let numbers = vec![3.0, 1.0, 1.0, 2.0, 2.0, 1.0];
1464 let problem = QuantumAnnealingUtils::create_number_partitioning_problem(&numbers);
1465
1466 assert_eq!(problem.num_spins, 6);
1467 assert!(problem
1468 .metadata
1469 .name
1470 .as_ref()
1471 .expect("metadata name should be set")
1472 .contains("Number Partitioning"));
1473 }
1474
1475 #[test]
1476 fn test_small_problem_annealing() {
1477 let problem = QuantumAnnealingUtils::create_random_ising_problem(3, 1.0, 1.0);
1478
1479 let config = QuantumAnnealingConfig {
1480 annealing_time: 1.0,
1481 time_steps: 100,
1482 topology: AnnealingTopology::Complete(3),
1483 enable_noise: false, ..Default::default()
1485 };
1486
1487 let mut simulator = QuantumAnnealingSimulator::new(config)
1488 .expect("should create quantum annealing simulator");
1489 simulator
1490 .set_problem(problem)
1491 .expect("should set problem successfully");
1492
1493 let result = simulator.anneal(10);
1494 assert!(result.is_ok());
1495
1496 let annealing_result = result.expect("should get annealing result");
1497 assert_eq!(annealing_result.solutions.len(), 10);
1498 assert!(!annealing_result.annealing_history.is_empty());
1499 }
1500
1501 #[test]
1502 fn test_field_strength_calculation() {
1503 let config = QuantumAnnealingConfig::default();
1504 let simulator = QuantumAnnealingSimulator::new(config)
1505 .expect("should create quantum annealing simulator");
1506
1507 let (transverse, longitudinal) = simulator.calculate_field_strengths(0.0);
1508 assert_abs_diff_eq!(transverse, 1.0, epsilon = 1e-10);
1509 assert_abs_diff_eq!(longitudinal, 0.0, epsilon = 1e-10);
1510
1511 let (transverse, longitudinal) = simulator.calculate_field_strengths(1.0);
1512 assert_abs_diff_eq!(transverse, 0.0, epsilon = 1e-10);
1513 assert_abs_diff_eq!(longitudinal, 1.0, epsilon = 1e-10);
1514 }
1515
1516 #[test]
1517 fn test_annealing_topologies() {
1518 let topologies = vec![
1519 AnnealingTopology::Chimera(4),
1520 AnnealingTopology::Pegasus(3),
1521 AnnealingTopology::Complete(5),
1522 ];
1523
1524 for topology in topologies {
1525 let config = QuantumAnnealingConfig {
1526 topology,
1527 ..Default::default()
1528 };
1529 let simulator = QuantumAnnealingSimulator::new(config);
1530 assert!(simulator.is_ok());
1531 }
1532 }
1533
1534 #[test]
1535 fn test_ising_ground_state_brute_force() {
1536 let mut problem = IsingProblem::new(2);
1538 problem.set_j(0, 1, -1.0); let (ground_state, ground_energy) = problem.find_ground_state_brute_force();
1541
1542 assert!(ground_state == vec![1, 1] || ground_state == vec![-1, -1]);
1544 assert_abs_diff_eq!(ground_energy, -1.0, epsilon = 1e-10);
1545 }
1546
1547 #[test]
1548 fn test_post_processing_config() {
1549 let config = PostProcessingConfig::default();
1550 assert!(config.enable_spin_reversal);
1551 assert!(config.enable_local_search);
1552 assert!(config.enable_majority_vote);
1553 assert_eq!(config.majority_vote_reads, 1000);
1554 }
1555}