quantrs2_sim/
quantum_annealing.rs

1//! Quantum annealing simulation with realistic noise models and hardware constraints.
2//!
3//! This module implements comprehensive quantum annealing simulation that accurately
4//! models real quantum annealing hardware including thermal noise, decoherence,
5//! control errors, and hardware topology constraints. It supports various
6//! optimization problems (QUBO, Ising, etc.) and provides realistic simulation
7//! of quantum annealing devices like D-Wave systems.
8
9use 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/// Quantum annealing configuration
21#[derive(Debug, Clone)]
22pub struct QuantumAnnealingConfig {
23    /// Total annealing time (μs)
24    pub annealing_time: f64,
25    /// Number of time steps
26    pub time_steps: usize,
27    /// Annealing schedule type
28    pub schedule_type: AnnealingScheduleType,
29    /// Problem formulation
30    pub problem_type: ProblemType,
31    /// Hardware topology
32    pub topology: AnnealingTopology,
33    /// Temperature (K)
34    pub temperature: f64,
35    /// Enable realistic noise models
36    pub enable_noise: bool,
37    /// Enable thermal fluctuations
38    pub enable_thermal_fluctuations: bool,
39    /// Enable control errors
40    pub enable_control_errors: bool,
41    /// Enable gauge transformations
42    pub enable_gauge_transformations: bool,
43    /// Post-processing configuration
44    pub post_processing: PostProcessingConfig,
45}
46
47impl Default for QuantumAnnealingConfig {
48    fn default() -> Self {
49        Self {
50            annealing_time: 20.0, // 20 μs (typical D-Wave)
51            time_steps: 2000,
52            schedule_type: AnnealingScheduleType::DWave,
53            problem_type: ProblemType::Ising,
54            topology: AnnealingTopology::Chimera(16),
55            temperature: 0.015, // 15 mK
56            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/// Annealing schedule types
66#[derive(Debug, Clone, Copy, PartialEq)]
67pub enum AnnealingScheduleType {
68    /// Linear schedule
69    Linear,
70    /// D-Wave like schedule with pause
71    DWave,
72    /// Optimized schedule for specific problems
73    Optimized,
74    /// Custom schedule with pause features
75    CustomPause {
76        pause_start: f64,
77        pause_duration: f64,
78    },
79    /// Non-monotonic schedule
80    NonMonotonic,
81    /// Reverse annealing
82    Reverse { reinitialize_point: f64 },
83}
84
85/// Problem types for quantum annealing
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum ProblemType {
88    /// Ising model
89    Ising,
90    /// Quadratic Unconstrained Binary Optimization
91    QUBO,
92    /// Maximum Cut
93    MaxCut,
94    /// Graph Coloring
95    GraphColoring,
96    /// Traveling Salesman Problem
97    TSP,
98    /// Number Partitioning
99    NumberPartitioning,
100    /// Custom optimization problem
101    Custom(String),
102}
103
104/// Annealing hardware topologies
105#[derive(Debug, Clone, PartialEq)]
106pub enum AnnealingTopology {
107    /// D-Wave Chimera topology
108    Chimera(usize), // Parameter is the size
109    /// D-Wave Pegasus topology
110    Pegasus(usize),
111    /// D-Wave Zephyr topology
112    Zephyr(usize),
113    /// Complete graph
114    Complete(usize),
115    /// Custom topology
116    Custom(DeviceTopology),
117}
118
119/// Post-processing configuration
120#[derive(Debug, Clone)]
121pub struct PostProcessingConfig {
122    /// Enable spin reversal transformations
123    pub enable_spin_reversal: bool,
124    /// Enable local search optimization
125    pub enable_local_search: bool,
126    /// Maximum local search iterations
127    pub max_local_search_iterations: usize,
128    /// Enable majority vote post-processing
129    pub enable_majority_vote: bool,
130    /// Number of reads for majority vote
131    pub majority_vote_reads: usize,
132    /// Enable energy-based filtering
133    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/// Ising problem representation
150#[derive(Debug, Clone)]
151pub struct IsingProblem {
152    /// Number of spins
153    pub num_spins: usize,
154    /// Linear coefficients (h_i)
155    pub h: Array1<f64>,
156    /// Quadratic coefficients (J_{ij})
157    pub j: Array2<f64>,
158    /// Offset constant
159    pub offset: f64,
160    /// Problem metadata
161    pub metadata: ProblemMetadata,
162}
163
164/// QUBO problem representation
165#[derive(Debug, Clone)]
166pub struct QUBOProblem {
167    /// Number of variables
168    pub num_variables: usize,
169    /// QUBO matrix (Q_{ij})
170    pub q: Array2<f64>,
171    /// Linear coefficients
172    pub linear: Array1<f64>,
173    /// Offset constant
174    pub offset: f64,
175    /// Problem metadata
176    pub metadata: ProblemMetadata,
177}
178
179/// Problem metadata
180#[derive(Debug, Clone, Default)]
181pub struct ProblemMetadata {
182    /// Problem name
183    pub name: Option<String>,
184    /// Problem description
185    pub description: Option<String>,
186    /// Known optimal energy
187    pub optimal_energy: Option<f64>,
188    /// Problem difficulty estimate
189    pub difficulty_score: Option<f64>,
190    /// Variable labels
191    pub variable_labels: Vec<String>,
192}
193
194impl IsingProblem {
195    /// Create new Ising problem
196    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    /// Set linear coefficient
207    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    /// Set quadratic coefficient
214    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; // Ensure symmetry
218        }
219    }
220
221    /// Calculate energy for a given configuration
222    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        // Linear terms
230        for i in 0..self.num_spins {
231            energy += self.h[i] * configuration[i] as f64;
232        }
233
234        // Quadratic terms
235        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    /// Convert to QUBO problem
245    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        // Convert Ising to QUBO: s_i = 2x_i - 1
252        // H_Ising = sum_i h_i s_i + sum_{i<j} J_{ij} s_i s_j
253        // H_QUBO = sum_i q_i x_i + sum_{i<j} q_{ij} x_i x_j + const
254
255        for i in 0..num_vars {
256            // Linear terms: h_i s_i = h_i (2x_i - 1) = 2h_i x_i - h_i
257            linear[i] += 2.0 * self.h[i];
258            offset -= self.h[i];
259
260            for j in i + 1..num_vars {
261                // Quadratic terms: J_{ij} s_i s_j = J_{ij} (2x_i - 1)(2x_j - 1)
262                // = 4 J_{ij} x_i x_j - 2 J_{ij} x_i - 2 J_{ij} x_j + J_{ij}
263                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    /// Find ground state using brute force (for small problems)
280    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    /// Create new QUBO problem
309    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    /// Calculate energy for a given binary configuration
320    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        // Linear terms
328        for i in 0..self.num_variables {
329            energy += self.linear[i] * configuration[i] as f64;
330        }
331
332        // Quadratic terms
333        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    /// Convert to Ising problem
345    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        // Convert QUBO to Ising: x_i = (s_i + 1)/2
352        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
380/// Quantum annealing simulator
381pub struct QuantumAnnealingSimulator {
382    /// Configuration
383    config: QuantumAnnealingConfig,
384    /// Current problem
385    current_problem: Option<IsingProblem>,
386    /// Device noise simulator
387    noise_simulator: Option<DeviceNoiseSimulator>,
388    /// SciRS2 backend for optimization
389    backend: Option<SciRS2Backend>,
390    /// Annealing history
391    annealing_history: Vec<AnnealingSnapshot>,
392    /// Final solutions
393    solutions: Vec<AnnealingSolution>,
394    /// Statistics
395    stats: AnnealingStats,
396}
397
398/// Annealing snapshot
399#[derive(Debug, Clone)]
400pub struct AnnealingSnapshot {
401    /// Time parameter
402    pub time: f64,
403    /// Annealing parameter s(t)
404    pub s: f64,
405    /// Transverse field strength
406    pub transverse_field: f64,
407    /// Longitudinal field strength
408    pub longitudinal_field: f64,
409    /// Current quantum state (if tracking)
410    pub quantum_state: Option<Array1<Complex64>>,
411    /// Classical state probabilities
412    pub classical_probabilities: Option<Array1<f64>>,
413    /// Energy expectation value
414    pub energy_expectation: f64,
415    /// Temperature effects
416    pub temperature_factor: f64,
417}
418
419/// Annealing solution
420#[derive(Debug, Clone)]
421pub struct AnnealingSolution {
422    /// Solution configuration
423    pub configuration: Vec<i8>,
424    /// Solution energy
425    pub energy: f64,
426    /// Solution probability
427    pub probability: f64,
428    /// Number of occurrences
429    pub num_occurrences: usize,
430    /// Solution rank
431    pub rank: usize,
432}
433
434/// Annealing simulation statistics
435#[derive(Debug, Clone, Default, Serialize, Deserialize)]
436pub struct AnnealingStats {
437    /// Total annealing time
438    pub total_annealing_time_ms: f64,
439    /// Number of annealing runs
440    pub num_annealing_runs: usize,
441    /// Number of solutions found
442    pub num_solutions_found: usize,
443    /// Best energy found
444    pub best_energy_found: f64,
445    /// Success probability (if ground state known)
446    pub success_probability: f64,
447    /// Time to solution statistics
448    pub time_to_solution: TimeToSolutionStats,
449    /// Noise statistics
450    pub noise_stats: NoiseStats,
451}
452
453/// Time to solution statistics
454#[derive(Debug, Clone, Default, Serialize, Deserialize)]
455pub struct TimeToSolutionStats {
456    /// Median time to solution
457    pub median_tts: f64,
458    /// 99th percentile time to solution
459    pub percentile_99_tts: f64,
460    /// Success rate
461    pub success_rate: f64,
462}
463
464/// Noise statistics
465#[derive(Debug, Clone, Default, Serialize, Deserialize)]
466pub struct NoiseStats {
467    /// Thermal excitation events
468    pub thermal_excitations: usize,
469    /// Control error events
470    pub control_errors: usize,
471    /// Decoherence events
472    pub decoherence_events: usize,
473}
474
475impl QuantumAnnealingSimulator {
476    /// Create new quantum annealing simulator
477    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    /// Initialize with SciRS2 backend
490    pub fn with_backend(mut self) -> Result<Self> {
491        self.backend = Some(SciRS2Backend::new());
492        Ok(self)
493    }
494
495    /// Set problem to solve
496    pub fn set_problem(&mut self, problem: IsingProblem) -> Result<()> {
497        // Validate problem size against topology
498        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    /// Run quantum annealing
518    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            // Run single annealing cycle
531            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        // Post-process solutions
545        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        // Sort solutions by energy
554        self.solutions
555            .sort_by(|a, b| a.energy.partial_cmp(&b.energy).unwrap());
556
557        // Rank solutions
558        for (rank, solution) in self.solutions.iter_mut().enumerate() {
559            solution.rank = rank;
560        }
561
562        // Compute statistics
563        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    /// Run single annealing cycle
584    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        // Initialize quantum state in superposition
588        let state_size = 1 << problem_num_spins.min(20); // Limit for memory
589        let mut quantum_state = if problem_num_spins <= 20 {
590            let mut state = Array1::zeros(state_size);
591            // Initialize in equal superposition
592            let amplitude = (1.0 / state_size as f64).sqrt();
593            state.fill(Complex64::new(amplitude, 0.0));
594            Some(state)
595        } else {
596            None // Use classical approximation for large problems
597        };
598
599        let dt = self.config.annealing_time / self.config.time_steps as f64;
600        self.annealing_history.clear();
601
602        // Annealing evolution
603        for step in 0..=self.config.time_steps {
604            let t = step as f64 * dt;
605            let s = self.schedule_function(t);
606
607            // Calculate field strengths
608            let (transverse_field, longitudinal_field) = self.calculate_field_strengths(s);
609
610            // Apply quantum evolution
611            if let Some(ref mut state) = quantum_state {
612                self.apply_quantum_evolution(state, transverse_field, longitudinal_field, dt)?;
613
614                // Apply noise if enabled
615                if self.config.enable_noise {
616                    self.apply_annealing_noise(state, dt)?;
617                }
618            }
619
620            // Take snapshot
621            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        // Final measurement/sampling
634        let final_configuration = if let Some(ref state) = quantum_state {
635            self.measure_final_state(state)?
636        } else {
637            // Get the problem again for classical sampling
638            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), // Will be updated later
652            num_occurrences: 1,
653            rank: 0,
654        })
655    }
656
657    /// Calculate annealing schedule s(t)
658    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                // D-Wave like schedule with slower start and end
665                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                // Optimized schedule based on problem characteristics
675                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 // Pause at this value
683                } 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                // Non-monotonic schedule with oscillations
691                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 // Start at problem Hamiltonian
699                } else {
700                    1.0 - (normalized_t - reinitialize_point) / (1.0 - reinitialize_point)
701                }
702            }
703        }
704    }
705
706    /// Optimized schedule function
707    fn optimized_schedule(&self, t: f64) -> f64 {
708        // Simple optimization: slower evolution near avoided crossings
709        // This would be problem-specific in practice
710        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    /// Calculate transverse and longitudinal field strengths
720    fn calculate_field_strengths(&self, s: f64) -> (f64, f64) {
721        // Standard quantum annealing: H(s) = -A(s) ∑_i σ_x^i + B(s) H_problem
722        let a_s = (1.0 - s) * 1.0; // Transverse field strength
723        let b_s = s * 1.0; // Longitudinal field strength
724        (a_s, b_s)
725    }
726
727    /// Apply quantum evolution for one time step
728    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        // Build total Hamiltonian matrix
739        let hamiltonian = self.build_annealing_hamiltonian(transverse_field, longitudinal_field)?;
740
741        // Apply time evolution: |ψ(t+dt)⟩ = exp(-i H dt / ℏ) |ψ(t)⟩
742        let evolution_operator = self.compute_evolution_operator(&hamiltonian, dt)?;
743        *state = evolution_operator.dot(state);
744
745        // Renormalize to handle numerical errors
746        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    /// Build full annealing Hamiltonian
755    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        // Transverse field terms: -A(s) ∑_i σ_x^i
766        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        // Longitudinal field terms: B(s) H_problem
772        let problem_hamiltonian = self.build_problem_hamiltonian()?;
773        hamiltonian = hamiltonian + problem_hamiltonian.mapv(|x| x * longitudinal_field);
774
775        Ok(hamiltonian)
776    }
777
778    /// Build Pauli-X operator for specific spin
779    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); // Flip the target spin
785            sigma_x[[i, j]] = Complex64::new(1.0, 0.0);
786        }
787
788        sigma_x
789    }
790
791    /// Build problem Hamiltonian (Ising model)
792    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        // Linear terms: ∑_i h_i σ_z^i
799        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        // Quadratic terms: ∑_{i<j} J_{ij} σ_z^i σ_z^j
805        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        // Add offset as identity matrix
817        for i in 0..dim {
818            hamiltonian[[i, i]] += Complex64::new(problem.offset, 0.0);
819        }
820
821        Ok(hamiltonian)
822    }
823
824    /// Build Pauli-Z operator for specific spin
825    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    /// Compute time evolution operator
842    fn compute_evolution_operator(
843        &self,
844        hamiltonian: &Array2<Complex64>,
845        dt: f64,
846    ) -> Result<Array2<Complex64>> {
847        // Use matrix exponentiation for small systems
848        self.matrix_exponential(hamiltonian, -Complex64::new(0.0, dt))
849    }
850
851    /// Matrix exponential implementation
852    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            // Limit series expansion
865            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    /// Apply various noise sources during annealing
879    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        // Decoherence
891        self.apply_decoherence_noise(state, dt)?;
892        self.stats.noise_stats.decoherence_events += 1;
893
894        Ok(())
895    }
896
897    /// Apply thermal noise
898    fn apply_thermal_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
899        // Thermal fluctuations cause random phase evolution
900        let kb_t = 1.38e-23 * self.config.temperature; // Boltzmann constant times temperature
901        let thermal_energy = kb_t * dt * 1e6; // Convert to relevant energy scale
902
903        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    /// Apply control error noise
912    fn apply_control_error_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
913        // Control errors cause imperfect implementation of the intended Hamiltonian
914        let error_strength = 0.01; // 1% control errors
915
916        // Apply random single-qubit rotations to simulate control errors
917        let problem = self.current_problem.as_ref().unwrap();
918        for spin in 0..problem.num_spins.min(10) {
919            // Limit for performance
920            if fastrand::f64() < error_strength * dt {
921                let error_angle = fastrand::f64() * 0.1; // Small random rotation
922                self.apply_single_spin_rotation(state, spin, error_angle)?;
923            }
924        }
925
926        Ok(())
927    }
928
929    /// Apply decoherence noise
930    fn apply_decoherence_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
931        let decoherence_rate = 1e-3; // Typical decoherence rate
932        let decoherence_prob = decoherence_rate * dt;
933
934        for amplitude in state.iter_mut() {
935            if fastrand::f64() < decoherence_prob {
936                // Apply random dephasing
937                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    /// Apply single spin rotation
946    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    /// Take annealing snapshot
974    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    /// Calculate energy expectation value
1003    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, &amplitude) in state.iter().enumerate() {
1008            let prob = amplitude.norm_sqr();
1009
1010            // Convert state index to spin configuration
1011            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    /// Measure final quantum state
1026    fn measure_final_state(&self, state: &Array1<Complex64>) -> Result<Vec<i8>> {
1027        let problem = self.current_problem.as_ref().unwrap();
1028
1029        // Sample from the quantum state probability distribution
1030        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                // Convert state index to spin configuration
1038                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        // Fallback to ground state
1049        Ok(vec![-1; problem.num_spins])
1050    }
1051
1052    /// Classical sampling for large problems
1053    fn classical_sampling(&self, problem: &IsingProblem) -> Result<Vec<i8>> {
1054        // Use simulated annealing or other classical heuristics
1055        let mut config: Vec<i8> = (0..problem.num_spins)
1056            .map(|_| if fastrand::f64() > 0.5 { 1 } else { -1 })
1057            .collect();
1058
1059        // Simple local search
1060        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; // Revert if energy increased
1069            }
1070        }
1071
1072        Ok(config)
1073    }
1074
1075    /// Apply majority vote post-processing
1076    fn apply_majority_vote_post_processing(&mut self) -> Result<()> {
1077        if self.solutions.is_empty() {
1078            return Ok(());
1079        }
1080
1081        // Group solutions by configuration
1082        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        // Update occurrence counts
1091        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    /// Apply local search post-processing
1102    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                    // Try flipping this spin
1114                    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; // Revert
1123                    }
1124                }
1125
1126                if !found_improvement {
1127                    break;
1128                }
1129            }
1130
1131            // Update solution if improved
1132            if improved_energy < solution.energy {
1133                solution.configuration = improved_config;
1134                solution.energy = improved_energy;
1135            }
1136        }
1137
1138        Ok(())
1139    }
1140
1141    /// Compute annealing statistics
1142    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        // Calculate success probability if ground state energy is known
1155        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    /// Get annealing statistics
1174    pub fn get_stats(&self) -> &AnnealingStats {
1175        &self.stats
1176    }
1177
1178    /// Reset statistics
1179    pub fn reset_stats(&mut self) {
1180        self.stats = AnnealingStats::default();
1181    }
1182}
1183
1184/// Annealing result
1185#[derive(Debug, Clone)]
1186pub struct AnnealingResult {
1187    /// All solutions found
1188    pub solutions: Vec<AnnealingSolution>,
1189    /// Best energy found
1190    pub best_energy: f64,
1191    /// Annealing evolution history
1192    pub annealing_history: Vec<AnnealingSnapshot>,
1193    /// Total computation time
1194    pub total_time_ms: f64,
1195    /// Success probability
1196    pub success_probability: f64,
1197    /// Time to solution statistics
1198    pub time_to_solution: TimeToSolutionStats,
1199}
1200
1201/// Quantum annealing utilities
1202pub struct QuantumAnnealingUtils;
1203
1204impl QuantumAnnealingUtils {
1205    /// Create Max-Cut Ising problem
1206    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            // Max-Cut: maximize ∑ w_{ij} (1 - s_i s_j) / 2
1220            // Equivalent to minimizing ∑ w_{ij} (s_i s_j - 1) / 2
1221            problem.set_j(u, v, weight / 2.0);
1222            problem.offset -= weight / 2.0;
1223        }
1224
1225        problem
1226    }
1227
1228    /// Create number partitioning problem
1229    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        // Minimize (∑_i n_i s_i)^2 = ∑_i n_i^2 + 2 ∑_{i<j} n_i n_j s_i s_j
1235        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    /// Create random Ising problem
1246    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        // Random linear coefficients
1255        for i in 0..num_spins {
1256            problem.set_h(i, (fastrand::f64() - 0.5) * 2.0 * h_range);
1257        }
1258
1259        // Random quadratic coefficients
1260        for i in 0..num_spins {
1261            for j in i + 1..num_spins {
1262                if fastrand::f64() < 0.5 {
1263                    // 50% sparsity
1264                    problem.set_j(i, j, (fastrand::f64() - 0.5) * 2.0 * j_range);
1265                }
1266            }
1267        }
1268
1269        problem
1270    }
1271
1272    /// Benchmark quantum annealing
1273    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]; // μs
1278
1279        for &size in &problem_sizes {
1280            for &time in &annealing_times {
1281                // Create random problem
1282                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/// Annealing benchmark results
1312#[derive(Debug, Clone, Default)]
1313pub struct AnnealingBenchmarkResults {
1314    /// Execution times by configuration
1315    pub execution_times: Vec<(String, f64)>,
1316    /// Best energies found
1317    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        // E = h_0 * s_0 + h_1 * s_1 + J_{01} * s_0 * s_1
1347        // E = 1.0 * 1 + (-0.5) * (-1) + 2.0 * 1 * (-1)
1348        // E = 1.0 + 0.5 - 2.0 = -0.5
1349        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        // Test energy equivalence for a configuration
1362        let ising_config = vec![1, -1];
1363        let qubo_config = vec![1, 0]; // s=1 -> x=1, s=-1 -> x=0
1364
1365        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, // Disable for deterministic test
1424            ..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        // Simple 2-spin ferromagnetic Ising model
1473        let mut problem = IsingProblem::new(2);
1474        problem.set_j(0, 1, -1.0); // Ferromagnetic coupling
1475
1476        let (ground_state, ground_energy) = problem.find_ground_state_brute_force();
1477
1478        // Ground states should be [1, 1] or [-1, -1] with energy -1
1479        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}