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 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/// 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    #[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    /// Set linear coefficient
208    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    /// Set quadratic coefficient
215    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; // Ensure symmetry
219        }
220    }
221
222    /// Calculate energy for a given configuration
223    #[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        // Linear terms
232        for i in 0..self.num_spins {
233            energy += self.h[i] * f64::from(configuration[i]);
234        }
235
236        // Quadratic terms
237        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    /// Convert to QUBO problem
248    #[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        // Convert Ising to QUBO: s_i = 2x_i - 1
256        // H_Ising = sum_i h_i s_i + sum_{i<j} J_{ij} s_i s_j
257        // H_QUBO = sum_i q_i x_i + sum_{i<j} q_{ij} x_i x_j + const
258
259        for i in 0..num_vars {
260            // Linear terms: h_i s_i = h_i (2x_i - 1) = 2h_i x_i - h_i
261            linear[i] += 2.0 * self.h[i];
262            offset -= self.h[i];
263
264            for j in i + 1..num_vars {
265                // Quadratic terms: J_{ij} s_i s_j = J_{ij} (2x_i - 1)(2x_j - 1)
266                // = 4 J_{ij} x_i x_j - 2 J_{ij} x_i - 2 J_{ij} x_j + J_{ij}
267                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    /// Find ground state using brute force (for small problems)
284    #[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    /// Create new QUBO problem
315    #[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    /// Calculate energy for a given binary configuration
327    #[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        // Linear terms
336        for i in 0..self.num_variables {
337            energy += self.linear[i] * f64::from(configuration[i]);
338        }
339
340        // Quadratic terms
341        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    /// Convert to Ising problem
354    #[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        // Convert QUBO to Ising: x_i = (s_i + 1)/2
362        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
390/// Quantum annealing simulator
391pub struct QuantumAnnealingSimulator {
392    /// Configuration
393    config: QuantumAnnealingConfig,
394    /// Current problem
395    current_problem: Option<IsingProblem>,
396    /// Device noise simulator
397    noise_simulator: Option<DeviceNoiseSimulator>,
398    /// `SciRS2` backend for optimization
399    backend: Option<SciRS2Backend>,
400    /// Annealing history
401    annealing_history: Vec<AnnealingSnapshot>,
402    /// Final solutions
403    solutions: Vec<AnnealingSolution>,
404    /// Statistics
405    stats: AnnealingStats,
406}
407
408/// Annealing snapshot
409#[derive(Debug, Clone)]
410pub struct AnnealingSnapshot {
411    /// Time parameter
412    pub time: f64,
413    /// Annealing parameter s(t)
414    pub s: f64,
415    /// Transverse field strength
416    pub transverse_field: f64,
417    /// Longitudinal field strength
418    pub longitudinal_field: f64,
419    /// Current quantum state (if tracking)
420    pub quantum_state: Option<Array1<Complex64>>,
421    /// Classical state probabilities
422    pub classical_probabilities: Option<Array1<f64>>,
423    /// Energy expectation value
424    pub energy_expectation: f64,
425    /// Temperature effects
426    pub temperature_factor: f64,
427}
428
429/// Annealing solution
430#[derive(Debug, Clone)]
431pub struct AnnealingSolution {
432    /// Solution configuration
433    pub configuration: Vec<i8>,
434    /// Solution energy
435    pub energy: f64,
436    /// Solution probability
437    pub probability: f64,
438    /// Number of occurrences
439    pub num_occurrences: usize,
440    /// Solution rank
441    pub rank: usize,
442}
443
444/// Annealing simulation statistics
445#[derive(Debug, Clone, Default, Serialize, Deserialize)]
446pub struct AnnealingStats {
447    /// Total annealing time
448    pub total_annealing_time_ms: f64,
449    /// Number of annealing runs
450    pub num_annealing_runs: usize,
451    /// Number of solutions found
452    pub num_solutions_found: usize,
453    /// Best energy found
454    pub best_energy_found: f64,
455    /// Success probability (if ground state known)
456    pub success_probability: f64,
457    /// Time to solution statistics
458    pub time_to_solution: TimeToSolutionStats,
459    /// Noise statistics
460    pub noise_stats: NoiseStats,
461}
462
463/// Time to solution statistics
464#[derive(Debug, Clone, Default, Serialize, Deserialize)]
465pub struct TimeToSolutionStats {
466    /// Median time to solution
467    pub median_tts: f64,
468    /// 99th percentile time to solution
469    pub percentile_99_tts: f64,
470    /// Success rate
471    pub success_rate: f64,
472}
473
474/// Noise statistics
475#[derive(Debug, Clone, Default, Serialize, Deserialize)]
476pub struct NoiseStats {
477    /// Thermal excitation events
478    pub thermal_excitations: usize,
479    /// Control error events
480    pub control_errors: usize,
481    /// Decoherence events
482    pub decoherence_events: usize,
483}
484
485impl QuantumAnnealingSimulator {
486    /// Create new quantum annealing simulator
487    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    /// Initialize with `SciRS2` backend
500    pub fn with_backend(mut self) -> Result<Self> {
501        self.backend = Some(SciRS2Backend::new());
502        Ok(self)
503    }
504
505    /// Set problem to solve
506    pub fn set_problem(&mut self, problem: IsingProblem) -> Result<()> {
507        // Validate problem size against topology
508        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    /// Run quantum annealing
528    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            // Run single annealing cycle
541            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        // Post-process solutions
555        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        // Sort solutions by energy
564        self.solutions.sort_by(|a, b| {
565            a.energy
566                .partial_cmp(&b.energy)
567                .unwrap_or(std::cmp::Ordering::Equal)
568        });
569
570        // Rank solutions
571        for (rank, solution) in self.solutions.iter_mut().enumerate() {
572            solution.rank = rank;
573        }
574
575        // Compute statistics
576        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    /// Run single annealing cycle
593    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        // Initialize quantum state in superposition
601        let state_size = 1 << problem_num_spins.min(20); // Limit for memory
602        let mut quantum_state = if problem_num_spins <= 20 {
603            let mut state = Array1::zeros(state_size);
604            // Initialize in equal superposition
605            let amplitude = (1.0 / state_size as f64).sqrt();
606            state.fill(Complex64::new(amplitude, 0.0));
607            Some(state)
608        } else {
609            None // Use classical approximation for large problems
610        };
611
612        let dt = self.config.annealing_time / self.config.time_steps as f64;
613        self.annealing_history.clear();
614
615        // Annealing evolution
616        for step in 0..=self.config.time_steps {
617            let t = step as f64 * dt;
618            let s = self.schedule_function(t);
619
620            // Calculate field strengths
621            let (transverse_field, longitudinal_field) = self.calculate_field_strengths(s);
622
623            // Apply quantum evolution
624            if let Some(ref mut state) = quantum_state {
625                self.apply_quantum_evolution(state, transverse_field, longitudinal_field, dt)?;
626
627                // Apply noise if enabled
628                if self.config.enable_noise {
629                    self.apply_annealing_noise(state, dt)?;
630                }
631            }
632
633            // Take snapshot
634            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        // Final measurement/sampling
647        let final_configuration = if let Some(ref state) = quantum_state {
648            self.measure_final_state(state)?
649        } else {
650            // Get the problem again for classical sampling
651            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), // Will be updated later
668            num_occurrences: 1,
669            rank: 0,
670        })
671    }
672
673    /// Calculate annealing schedule s(t)
674    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                // D-Wave like schedule with slower start and end
681                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                // Optimized schedule based on problem characteristics
694                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 // Pause at this value
702                } 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                // Non-monotonic schedule with oscillations
710                (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 // Start at problem Hamiltonian
716                } else {
717                    1.0 - (normalized_t - reinitialize_point) / (1.0 - reinitialize_point)
718                }
719            }
720        }
721    }
722
723    /// Optimized schedule function
724    fn optimized_schedule(&self, t: f64) -> f64 {
725        // Simple optimization: slower evolution near avoided crossings
726        // This would be problem-specific in practice
727        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    /// Calculate transverse and longitudinal field strengths
737    fn calculate_field_strengths(&self, s: f64) -> (f64, f64) {
738        // Standard quantum annealing: H(s) = -A(s) ∑_i σ_x^i + B(s) H_problem
739        let a_s = (1.0 - s) * 1.0; // Transverse field strength
740        let b_s = s * 1.0; // Longitudinal field strength
741        (a_s, b_s)
742    }
743
744    /// Apply quantum evolution for one time step
745    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        // Problem is guaranteed to exist when called from single_anneal
753        let _problem = self
754            .current_problem
755            .as_ref()
756            .ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
757
758        // Build total Hamiltonian matrix
759        let hamiltonian = self.build_annealing_hamiltonian(transverse_field, longitudinal_field)?;
760
761        // Apply time evolution: |ψ(t+dt)⟩ = exp(-i H dt / ℏ) |ψ(t)⟩
762        let evolution_operator = self.compute_evolution_operator(&hamiltonian, dt)?;
763        *state = evolution_operator.dot(state);
764
765        // Renormalize to handle numerical errors
766        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    /// Build full annealing Hamiltonian
779    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        // Transverse field terms: -A(s) ∑_i σ_x^i
793        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        // Longitudinal field terms: B(s) H_problem
799        let problem_hamiltonian = self.build_problem_hamiltonian()?;
800        hamiltonian = hamiltonian + problem_hamiltonian.mapv(|x| x * longitudinal_field);
801
802        Ok(hamiltonian)
803    }
804
805    /// Build Pauli-X operator for specific spin
806    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); // Flip the target spin
812            sigma_x[[i, j]] = Complex64::new(1.0, 0.0);
813        }
814
815        sigma_x
816    }
817
818    /// Build problem Hamiltonian (Ising model)
819    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        // Linear terms: ∑_i h_i σ_z^i
829        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        // Quadratic terms: ∑_{i<j} J_{ij} σ_z^i σ_z^j
835        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        // Add offset as identity matrix
847        for i in 0..dim {
848            hamiltonian[[i, i]] += Complex64::new(problem.offset, 0.0);
849        }
850
851        Ok(hamiltonian)
852    }
853
854    /// Build Pauli-Z operator for specific spin
855    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    /// Compute time evolution operator
872    fn compute_evolution_operator(
873        &self,
874        hamiltonian: &Array2<Complex64>,
875        dt: f64,
876    ) -> Result<Array2<Complex64>> {
877        // Use matrix exponentiation for small systems
878        self.matrix_exponential(hamiltonian, -Complex64::new(0.0, dt))
879    }
880
881    /// Matrix exponential implementation
882    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            // Limit series expansion
895            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    /// Apply various noise sources during annealing
913    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        // Decoherence
925        self.apply_decoherence_noise(state, dt)?;
926        self.stats.noise_stats.decoherence_events += 1;
927
928        Ok(())
929    }
930
931    /// Apply thermal noise
932    fn apply_thermal_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
933        // Thermal fluctuations cause random phase evolution
934        let kb_t = 1.38e-23 * self.config.temperature; // Boltzmann constant times temperature
935        let thermal_energy = kb_t * dt * 1e6; // Convert to relevant energy scale
936
937        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    /// Apply control error noise
946    fn apply_control_error_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
947        // Control errors cause imperfect implementation of the intended Hamiltonian
948        let error_strength = 0.01; // 1% control errors
949
950        // Apply random single-qubit rotations to simulate control errors
951        // Problem is guaranteed to exist when called from apply_annealing_noise
952        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            // Limit for performance
958            if fastrand::f64() < error_strength * dt {
959                let error_angle = fastrand::f64() * 0.1; // Small random rotation
960                self.apply_single_spin_rotation(state, spin, error_angle)?;
961            }
962        }
963
964        Ok(())
965    }
966
967    /// Apply decoherence noise
968    fn apply_decoherence_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
969        let decoherence_rate = 1e-3; // Typical decoherence rate
970        let decoherence_prob = decoherence_rate * dt;
971
972        for amplitude in state.iter_mut() {
973            if fastrand::f64() < decoherence_prob {
974                // Apply random dephasing
975                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    /// Apply single spin rotation
984    fn apply_single_spin_rotation(
985        &self,
986        state: &mut Array1<Complex64>,
987        spin: usize,
988        angle: f64,
989    ) -> Result<()> {
990        // Problem check for validation - spin_mask doesn't depend on it but kept for API consistency
991        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    /// Take annealing snapshot
1016    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    /// Calculate energy expectation value
1045    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, &amplitude) in state.iter().enumerate() {
1053            let prob = amplitude.norm_sqr();
1054
1055            // Convert state index to spin configuration
1056            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    /// Measure final quantum state
1071    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        // Sample from the quantum state probability distribution
1078        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                // Convert state index to spin configuration
1086                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        // Fallback to ground state
1097        Ok(vec![-1; problem.num_spins])
1098    }
1099
1100    /// Classical sampling for large problems
1101    fn classical_sampling(&self, problem: &IsingProblem) -> Result<Vec<i8>> {
1102        // Use simulated annealing or other classical heuristics
1103        let mut config: Vec<i8> = (0..problem.num_spins)
1104            .map(|_| if fastrand::f64() > 0.5 { 1 } else { -1 })
1105            .collect();
1106
1107        // Simple local search
1108        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; // Revert if energy increased
1117            }
1118        }
1119
1120        Ok(config)
1121    }
1122
1123    /// Apply majority vote post-processing
1124    fn apply_majority_vote_post_processing(&mut self) -> Result<()> {
1125        if self.solutions.is_empty() {
1126            return Ok(());
1127        }
1128
1129        // Group solutions by configuration
1130        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        // Update occurrence counts
1139        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    /// Apply local search post-processing
1150    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                    // Try flipping this spin
1165                    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; // Revert
1174                }
1175
1176                if !found_improvement {
1177                    break;
1178                }
1179            }
1180
1181            // Update solution if improved
1182            if improved_energy < solution.energy {
1183                solution.configuration = improved_config;
1184                solution.energy = improved_energy;
1185            }
1186        }
1187
1188        Ok(())
1189    }
1190
1191    /// Compute annealing statistics
1192    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        // Calculate success probability if ground state energy is known
1205        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    /// Get annealing statistics
1224    #[must_use]
1225    pub const fn get_stats(&self) -> &AnnealingStats {
1226        &self.stats
1227    }
1228
1229    /// Reset statistics
1230    pub fn reset_stats(&mut self) {
1231        self.stats = AnnealingStats::default();
1232    }
1233}
1234
1235/// Annealing result
1236#[derive(Debug, Clone)]
1237pub struct AnnealingResult {
1238    /// All solutions found
1239    pub solutions: Vec<AnnealingSolution>,
1240    /// Best energy found
1241    pub best_energy: f64,
1242    /// Annealing evolution history
1243    pub annealing_history: Vec<AnnealingSnapshot>,
1244    /// Total computation time
1245    pub total_time_ms: f64,
1246    /// Success probability
1247    pub success_probability: f64,
1248    /// Time to solution statistics
1249    pub time_to_solution: TimeToSolutionStats,
1250}
1251
1252/// Quantum annealing utilities
1253pub struct QuantumAnnealingUtils;
1254
1255impl QuantumAnnealingUtils {
1256    /// Create Max-Cut Ising problem
1257    #[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            // Max-Cut: maximize ∑ w_{ij} (1 - s_i s_j) / 2
1272            // Equivalent to minimizing ∑ w_{ij} (s_i s_j - 1) / 2
1273            problem.set_j(u, v, weight / 2.0);
1274            problem.offset -= weight / 2.0;
1275        }
1276
1277        problem
1278    }
1279
1280    /// Create number partitioning problem
1281    #[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        // Minimize (∑_i n_i s_i)^2 = ∑_i n_i^2 + 2 ∑_{i<j} n_i n_j s_i s_j
1288        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    /// Create random Ising problem
1299    #[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        // Random linear coefficients
1309        for i in 0..num_spins {
1310            problem.set_h(i, (fastrand::f64() - 0.5) * 2.0 * h_range);
1311        }
1312
1313        // Random quadratic coefficients
1314        for i in 0..num_spins {
1315            for j in i + 1..num_spins {
1316                if fastrand::f64() < 0.5 {
1317                    // 50% sparsity
1318                    problem.set_j(i, j, (fastrand::f64() - 0.5) * 2.0 * j_range);
1319                }
1320            }
1321        }
1322
1323        problem
1324    }
1325
1326    /// Benchmark quantum annealing
1327    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]; // μs
1332
1333        for &size in &problem_sizes {
1334            for &time in &annealing_times {
1335                // Create random problem
1336                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/// Annealing benchmark results
1366#[derive(Debug, Clone, Default)]
1367pub struct AnnealingBenchmarkResults {
1368    /// Execution times by configuration
1369    pub execution_times: Vec<(String, f64)>,
1370    /// Best energies found
1371    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        // E = h_0 * s_0 + h_1 * s_1 + J_{01} * s_0 * s_1
1401        // E = 1.0 * 1 + (-0.5) * (-1) + 2.0 * 1 * (-1)
1402        // E = 1.0 + 0.5 - 2.0 = -0.5
1403        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        // Test energy equivalence for a configuration
1416        let ising_config = vec![1, -1];
1417        let qubo_config = vec![1, 0]; // s=1 -> x=1, s=-1 -> x=0
1418
1419        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, // Disable for deterministic test
1484            ..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        // Simple 2-spin ferromagnetic Ising model
1537        let mut problem = IsingProblem::new(2);
1538        problem.set_j(0, 1, -1.0); // Ferromagnetic coupling
1539
1540        let (ground_state, ground_energy) = problem.find_ground_state_brute_force();
1541
1542        // Ground states should be [1, 1] or [-1, -1] with energy -1
1543        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}