quantrs2_core/
adiabatic.rs

1//! Adiabatic Quantum Computing Simulation
2//!
3//! This module implements adiabatic quantum computing (AQC) and quantum annealing
4//! algorithms for solving optimization problems. It includes:
5//! - Adiabatic evolution under time-dependent Hamiltonians
6//! - Quantum annealing for combinatorial optimization
7//! - Problem encodings (QUBO, Ising model, etc.)
8//! - Annealing schedules and gap analysis
9//! - Performance monitoring and optimization
10
11#![allow(unpredictable_function_pointer_comparisons)]
12
13use crate::error::{QuantRS2Error, QuantRS2Result};
14use scirs2_core::ndarray::{Array1, Array2};
15use scirs2_core::random::prelude::*;
16use scirs2_core::Complex64;
17use std::f64::consts::PI;
18
19/// Types of optimization problems for adiabatic quantum computing
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum ProblemType {
22    /// Quadratic Unconstrained Binary Optimization
23    QUBO,
24    /// Ising spin glass model
25    Ising,
26    /// Maximum Cut problem
27    MaxCut,
28    /// Graph Coloring
29    GraphColoring,
30    /// Traveling Salesman Problem
31    TSP,
32    /// Number Partitioning
33    NumberPartitioning,
34    /// Custom problem with user-defined Hamiltonian
35    Custom,
36}
37
38/// Annealing schedule types
39#[derive(Debug, Clone, Copy)]
40pub enum AnnealingSchedule {
41    /// Linear annealing: s(t) = t/T
42    Linear,
43    /// Exponential annealing
44    Exponential { rate: f64 },
45    /// Polynomial annealing: s(t) = (t/T)^p
46    Polynomial { power: f64 },
47    /// Trigonometric annealing: s(t) = sin²(πt/2T)
48    Trigonometric,
49    /// Custom schedule with user-defined function
50    Custom(fn(f64, f64) -> f64),
51}
52
53impl AnnealingSchedule {
54    /// Evaluate the schedule at time t with total time T
55    pub fn evaluate(&self, t: f64, total_time: f64) -> f64 {
56        let s = t / total_time;
57        match self {
58            AnnealingSchedule::Linear => s,
59            AnnealingSchedule::Exponential { rate } => 1.0 - (-rate * s).exp(),
60            AnnealingSchedule::Polynomial { power } => s.powf(*power),
61            AnnealingSchedule::Trigonometric => (PI * s / 2.0).sin().powi(2),
62            AnnealingSchedule::Custom(func) => func(t, total_time),
63        }
64    }
65
66    /// Get the derivative of the schedule
67    pub fn derivative(&self, t: f64, total_time: f64) -> f64 {
68        let dt = 1e-8;
69        let s1 = self.evaluate(t + dt, total_time);
70        let s0 = self.evaluate(t, total_time);
71        (s1 - s0) / dt
72    }
73}
74
75/// QUBO (Quadratic Unconstrained Binary Optimization) problem representation
76#[derive(Debug, Clone)]
77pub struct QUBOProblem {
78    /// Number of variables
79    pub num_vars: usize,
80    /// Q matrix: objective is x^T Q x
81    pub q_matrix: Array2<f64>,
82    /// Linear terms (optional)
83    pub linear_terms: Option<Array1<f64>>,
84    /// Constant offset
85    pub offset: f64,
86}
87
88impl QUBOProblem {
89    /// Create a new QUBO problem
90    pub fn new(q_matrix: Array2<f64>) -> QuantRS2Result<Self> {
91        let (rows, cols) = q_matrix.dim();
92        if rows != cols {
93            return Err(QuantRS2Error::InvalidInput(
94                "Q matrix must be square".to_string(),
95            ));
96        }
97
98        Ok(Self {
99            num_vars: rows,
100            q_matrix,
101            linear_terms: None,
102            offset: 0.0,
103        })
104    }
105
106    /// Add linear terms to the QUBO problem
107    pub fn with_linear_terms(mut self, linear: Array1<f64>) -> QuantRS2Result<Self> {
108        if linear.len() != self.num_vars {
109            return Err(QuantRS2Error::InvalidInput(
110                "Linear terms size mismatch".to_string(),
111            ));
112        }
113        self.linear_terms = Some(linear);
114        Ok(self)
115    }
116
117    /// Add constant offset
118    pub fn with_offset(mut self, offset: f64) -> Self {
119        self.offset = offset;
120        self
121    }
122
123    /// Evaluate the QUBO objective for a binary solution
124    pub fn evaluate(&self, solution: &[u8]) -> QuantRS2Result<f64> {
125        if solution.len() != self.num_vars {
126            return Err(QuantRS2Error::InvalidInput(
127                "Solution size mismatch".to_string(),
128            ));
129        }
130
131        let mut objective = self.offset;
132
133        // Quadratic terms
134        for i in 0..self.num_vars {
135            for j in 0..self.num_vars {
136                objective += self.q_matrix[[i, j]] * solution[i] as f64 * solution[j] as f64;
137            }
138        }
139
140        // Linear terms
141        if let Some(ref linear) = self.linear_terms {
142            for i in 0..self.num_vars {
143                objective += linear[i] * solution[i] as f64;
144            }
145        }
146
147        Ok(objective)
148    }
149
150    /// Convert to Ising model (spins ∈ {-1, +1})
151    pub fn to_ising(&self) -> IsingProblem {
152        let mut h = Array1::zeros(self.num_vars);
153        let mut j = Array2::zeros((self.num_vars, self.num_vars));
154        let mut offset = self.offset;
155
156        // Transform QUBO to Ising: x_i = (1 + s_i)/2
157        for i in 0..self.num_vars {
158            // Linear terms: Q_ii * (1 + s_i)/2 + h_i * (1 + s_i)/2
159            h[i] = self.q_matrix[[i, i]] / 4.0;
160            offset += self.q_matrix[[i, i]] / 4.0;
161
162            if let Some(ref linear) = self.linear_terms {
163                h[i] += linear[i] / 2.0;
164                offset += linear[i] / 2.0;
165            }
166
167            // Quadratic terms: Q_ij * (1 + s_i)/2 * (1 + s_j)/2
168            for k in 0..self.num_vars {
169                if i != k {
170                    j[[i, k]] = self.q_matrix[[i, k]] / 4.0;
171                    h[i] += self.q_matrix[[i, k]] / 4.0;
172                    h[k] += self.q_matrix[[i, k]] / 4.0;
173                    offset += self.q_matrix[[i, k]] / 4.0;
174                }
175            }
176        }
177
178        IsingProblem {
179            num_spins: self.num_vars,
180            h_fields: h,
181            j_couplings: j,
182            offset,
183        }
184    }
185}
186
187/// Ising model problem representation
188#[derive(Debug, Clone)]
189pub struct IsingProblem {
190    /// Number of spins
191    pub num_spins: usize,
192    /// Local magnetic fields (h_i)
193    pub h_fields: Array1<f64>,
194    /// Coupling matrix (J_ij)
195    pub j_couplings: Array2<f64>,
196    /// Constant offset
197    pub offset: f64,
198}
199
200impl IsingProblem {
201    /// Create a new Ising problem
202    pub fn new(h_fields: Array1<f64>, j_couplings: Array2<f64>) -> QuantRS2Result<Self> {
203        let num_spins = h_fields.len();
204        let (rows, cols) = j_couplings.dim();
205
206        if rows != num_spins || cols != num_spins {
207            return Err(QuantRS2Error::InvalidInput(
208                "Coupling matrix size mismatch".to_string(),
209            ));
210        }
211
212        Ok(Self {
213            num_spins,
214            h_fields,
215            j_couplings,
216            offset: 0.0,
217        })
218    }
219
220    /// Evaluate the Ising energy for a spin configuration
221    pub fn evaluate(&self, spins: &[i8]) -> QuantRS2Result<f64> {
222        if spins.len() != self.num_spins {
223            return Err(QuantRS2Error::InvalidInput(
224                "Spin configuration size mismatch".to_string(),
225            ));
226        }
227
228        let mut energy = self.offset;
229
230        // Local field terms: -h_i * s_i
231        for i in 0..self.num_spins {
232            energy -= self.h_fields[i] * spins[i] as f64;
233        }
234
235        // Coupling terms: -J_ij * s_i * s_j
236        for i in 0..self.num_spins {
237            for j in i + 1..self.num_spins {
238                energy -= self.j_couplings[[i, j]] * spins[i] as f64 * spins[j] as f64;
239            }
240        }
241
242        Ok(energy)
243    }
244
245    /// Generate problem Hamiltonian as a matrix
246    pub fn hamiltonian(&self) -> Array2<Complex64> {
247        let dim = 1 << self.num_spins;
248        let mut hamiltonian = Array2::zeros((dim, dim));
249
250        for state in 0..dim {
251            let spins = self.state_to_spins(state);
252            let energy = self.evaluate(&spins).unwrap_or(0.0);
253            hamiltonian[[state, state]] = Complex64::new(energy, 0.0);
254        }
255
256        hamiltonian
257    }
258
259    /// Convert computational basis state to spin configuration
260    fn state_to_spins(&self, state: usize) -> Vec<i8> {
261        (0..self.num_spins)
262            .map(|i| if (state >> i) & 1 == 0 { -1 } else { 1 })
263            .collect()
264    }
265
266    /// Convert spin configuration to computational basis state
267    fn spins_to_state(&self, spins: &[i8]) -> usize {
268        spins.iter().enumerate().fold(0, |acc, (i, &spin)| {
269            acc | (if spin == 1 { 1 } else { 0 }) << i
270        })
271    }
272}
273
274/// Adiabatic quantum computer simulator
275pub struct AdiabaticQuantumComputer {
276    /// Problem Hamiltonian (H_P)
277    problem_hamiltonian: Array2<Complex64>,
278    /// Initial Hamiltonian (H_0) - typically transverse field
279    initial_hamiltonian: Array2<Complex64>,
280    /// Current quantum state
281    state: Array1<Complex64>,
282    /// Number of qubits/spins
283    num_qubits: usize,
284    /// Total annealing time
285    total_time: f64,
286    /// Current time
287    current_time: f64,
288    /// Time step for evolution
289    time_step: f64,
290    /// Annealing schedule
291    schedule: AnnealingSchedule,
292}
293
294impl AdiabaticQuantumComputer {
295    /// Create a new adiabatic quantum computer
296    pub fn new(
297        problem: &IsingProblem,
298        total_time: f64,
299        time_step: f64,
300        schedule: AnnealingSchedule,
301    ) -> Self {
302        let num_qubits = problem.num_spins;
303        let dim = 1 << num_qubits;
304
305        // Problem Hamiltonian
306        let problem_hamiltonian = problem.hamiltonian();
307
308        // Initial Hamiltonian: H_0 = -∑_i σ_x^i (transverse field)
309        let mut initial_hamiltonian = Array2::zeros((dim, dim));
310        for i in 0..num_qubits {
311            let pauli_x = Self::pauli_x_tensor(i, num_qubits);
312            initial_hamiltonian = initial_hamiltonian + pauli_x;
313        }
314        initial_hamiltonian = initial_hamiltonian.mapv(|x: Complex64| -x);
315
316        // Initial state: uniform superposition (ground state of H_0)
317        let mut state = Array1::zeros(dim);
318        let amplitude = Complex64::new(1.0 / (dim as f64).sqrt(), 0.0);
319        state.fill(amplitude);
320
321        Self {
322            problem_hamiltonian,
323            initial_hamiltonian,
324            state,
325            num_qubits,
326            total_time,
327            current_time: 0.0,
328            time_step,
329            schedule,
330        }
331    }
332
333    /// Generate Pauli-X tensor product for qubit i
334    fn pauli_x_tensor(target_qubit: usize, num_qubits: usize) -> Array2<Complex64> {
335        let dim = 1 << num_qubits;
336        let mut result = Array2::zeros((dim, dim));
337
338        for state in 0..dim {
339            // Flip bit at target_qubit position
340            let flipped_state = state ^ (1 << target_qubit);
341            result[[state, flipped_state]] = Complex64::new(1.0, 0.0);
342        }
343
344        result
345    }
346
347    /// Get the current Hamiltonian H(t) = (1-s(t))H_0 + s(t)H_P
348    pub fn current_hamiltonian(&self) -> Array2<Complex64> {
349        let s = self.schedule.evaluate(self.current_time, self.total_time);
350        let one_minus_s = 1.0 - s;
351
352        self.initial_hamiltonian.mapv(|x| x * one_minus_s)
353            + self.problem_hamiltonian.mapv(|x| x * s)
354    }
355
356    /// Perform one time step of adiabatic evolution
357    pub fn step(&mut self) -> QuantRS2Result<()> {
358        if self.current_time >= self.total_time {
359            return Ok(());
360        }
361
362        let hamiltonian = self.current_hamiltonian();
363
364        // Apply time evolution: |ψ(t+dt)⟩ = exp(-iH(t)dt)|ψ(t)⟩
365        // Using first-order approximation: exp(-iHdt) ≈ I - iHdt
366        let dt = self.time_step.min(self.total_time - self.current_time);
367        let evolution_operator = self.compute_evolution_operator(&hamiltonian, dt)?;
368
369        self.state = evolution_operator.dot(&self.state);
370        self.current_time += dt;
371
372        // Normalize state
373        let norm = self.state.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
374        if norm > 0.0 {
375            self.state.mapv_inplace(|c| c / norm);
376        }
377
378        Ok(())
379    }
380
381    /// Compute evolution operator exp(-iHdt) using matrix exponentiation
382    fn compute_evolution_operator(
383        &self,
384        hamiltonian: &Array2<Complex64>,
385        dt: f64,
386    ) -> QuantRS2Result<Array2<Complex64>> {
387        let dim = hamiltonian.nrows();
388        let mut evolution = Array2::eye(dim);
389
390        // Use series expansion: exp(-iHdt) = I - iHdt - (Hdt)²/2! + i(Hdt)³/3! + ...
391        let mut term = Array2::eye(dim);
392        let h_dt = hamiltonian.mapv(|h| Complex64::new(0.0, -dt) * h);
393
394        for n in 1..20 {
395            // Truncate series at 20 terms
396            term = term.dot(&h_dt);
397            let coefficient = 1.0 / (1..=n).fold(1.0, |acc, i| acc * i as f64);
398            evolution = evolution + term.mapv(|t| t * coefficient);
399
400            // Check convergence
401            let term_norm = term.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
402            if term_norm * coefficient < 1e-12 {
403                break;
404            }
405        }
406
407        Ok(evolution)
408    }
409
410    /// Run the complete adiabatic evolution
411    pub fn run(&mut self) -> QuantRS2Result<()> {
412        while self.current_time < self.total_time {
413            self.step()?;
414        }
415        Ok(())
416    }
417
418    /// Get measurement probabilities in computational basis
419    pub fn measurement_probabilities(&self) -> Vec<f64> {
420        self.state.iter().map(|c| c.norm_sqr()).collect()
421    }
422
423    /// Sample a measurement result
424    pub fn measure(&self) -> usize {
425        let mut rng = thread_rng();
426        let probs = self.measurement_probabilities();
427
428        let random_value: f64 = rng.gen();
429        let mut cumulative = 0.0;
430
431        for (state, prob) in probs.iter().enumerate() {
432            cumulative += prob;
433            if random_value <= cumulative {
434                return state;
435            }
436        }
437
438        probs.len() - 1 // Fallback
439    }
440
441    /// Get the instantaneous energy gap
442    pub fn energy_gap(&self) -> QuantRS2Result<f64> {
443        let hamiltonian = self.current_hamiltonian();
444
445        // For small systems, compute eigenvalues directly
446        if self.num_qubits <= 8 {
447            let eigenvalues = self.compute_eigenvalues(&hamiltonian)?;
448            let ground_energy = eigenvalues[0];
449            let first_excited = eigenvalues[1];
450            Ok(first_excited - ground_energy)
451        } else {
452            // For larger systems, use approximation
453            Ok(self.estimate_gap(&hamiltonian))
454        }
455    }
456
457    /// Compute eigenvalues (simplified implementation)
458    fn compute_eigenvalues(&self, hamiltonian: &Array2<Complex64>) -> QuantRS2Result<Vec<f64>> {
459        // This is a placeholder - in practice you'd use a proper eigenvalue solver
460        let dim = hamiltonian.nrows();
461        let mut eigenvalues: Vec<f64> = (0..dim).map(|i| hamiltonian[[i, i]].re).collect();
462        eigenvalues.sort_by(|a, b| {
463            a.partial_cmp(b)
464                .expect("Failed to compare eigenvalues in compute_eigenvalues")
465        });
466        Ok(eigenvalues)
467    }
468
469    /// Estimate energy gap using power iteration
470    fn estimate_gap(&self, _hamiltonian: &Array2<Complex64>) -> f64 {
471        // Simplified estimation - would use proper numerical methods in practice
472        let s = self.schedule.evaluate(self.current_time, self.total_time);
473        // Gap typically closes as sin(πs) near the end of annealing
474        (PI * s).sin() * 0.1 // Rough approximation
475    }
476
477    /// Check if adiabatic condition is satisfied
478    pub fn adiabatic_condition_satisfied(&self) -> QuantRS2Result<bool> {
479        let gap = self.energy_gap()?;
480        let s_dot = self.schedule.derivative(self.current_time, self.total_time);
481
482        // Adiabatic condition: |⟨1|dH/dt|0⟩| << Δ²
483        // Simplified check: gap should be much larger than evolution rate
484        Ok(gap > 10.0 * s_dot.abs())
485    }
486
487    /// Reset to initial state
488    pub fn reset(&mut self) {
489        let dim = 1 << self.num_qubits;
490        let amplitude = Complex64::new(1.0 / (dim as f64).sqrt(), 0.0);
491        self.state.fill(amplitude);
492        self.current_time = 0.0;
493    }
494
495    /// Get current state vector
496    pub fn state(&self) -> &Array1<Complex64> {
497        &self.state
498    }
499
500    /// Get current annealing parameter s(t)
501    pub fn annealing_parameter(&self) -> f64 {
502        self.schedule.evaluate(self.current_time, self.total_time)
503    }
504
505    /// Get progress as percentage
506    pub fn progress(&self) -> f64 {
507        (self.current_time / self.total_time).min(1.0) * 100.0
508    }
509}
510
511/// Quantum annealing optimizer for solving optimization problems
512pub struct QuantumAnnealer {
513    computer: AdiabaticQuantumComputer,
514    problem: IsingProblem,
515    best_solution: Option<(Vec<i8>, f64)>,
516    history: Vec<QuantumAnnealingSnapshot>,
517}
518
519/// Snapshot of quantum annealing state at a point in time
520#[derive(Debug, Clone)]
521pub struct QuantumAnnealingSnapshot {
522    pub time: f64,
523    pub annealing_parameter: f64,
524    pub energy_gap: f64,
525    pub ground_state_probability: f64,
526    pub expected_energy: f64,
527}
528
529impl QuantumAnnealer {
530    /// Create a new quantum annealer
531    pub fn new(
532        problem: IsingProblem,
533        total_time: f64,
534        time_step: f64,
535        schedule: AnnealingSchedule,
536    ) -> Self {
537        let computer = AdiabaticQuantumComputer::new(&problem, total_time, time_step, schedule);
538
539        Self {
540            computer,
541            problem,
542            best_solution: None,
543            history: Vec::new(),
544        }
545    }
546
547    /// Run quantum annealing optimization
548    pub fn optimize(&mut self) -> QuantRS2Result<(Vec<i8>, f64)> {
549        self.computer.reset();
550        self.history.clear();
551
552        // Record initial state
553        self.record_snapshot()?;
554
555        // Run adiabatic evolution
556        while self.computer.current_time < self.computer.total_time {
557            self.computer.step()?;
558
559            // Record snapshots at regular intervals
560            if self.history.len() % 10 == 0 {
561                self.record_snapshot()?;
562            }
563        }
564
565        // Final measurement and solution extraction
566        let measurement = self.computer.measure();
567        let spins = self.state_to_spins(measurement);
568        let energy = self.problem.evaluate(&spins)?;
569
570        self.best_solution = Some((spins.clone(), energy));
571
572        Ok((spins, energy))
573    }
574
575    /// Run multiple annealing runs and return the best solution
576    pub fn optimize_multiple_runs(&mut self, num_runs: usize) -> QuantRS2Result<(Vec<i8>, f64)> {
577        let mut best_energy = f64::INFINITY;
578        let mut best_spins = vec![];
579
580        for _ in 0..num_runs {
581            let (spins, energy) = self.optimize()?;
582            if energy < best_energy {
583                best_energy = energy;
584                best_spins = spins;
585            }
586        }
587
588        Ok((best_spins, best_energy))
589    }
590
591    /// Record a snapshot of the current annealing state
592    fn record_snapshot(&mut self) -> QuantRS2Result<()> {
593        let time = self.computer.current_time;
594        let annealing_parameter = self.computer.annealing_parameter();
595        let energy_gap = self.computer.energy_gap()?;
596
597        // Calculate ground state probability and expected energy
598        let probs = self.computer.measurement_probabilities();
599        let ground_state_probability = probs[0]; // Assuming ground state is first
600
601        let mut expected_energy = 0.0;
602        for (state_idx, &prob) in probs.iter().enumerate() {
603            let spins = self.state_to_spins(state_idx);
604            let energy = self.problem.evaluate(&spins).unwrap_or(0.0);
605            expected_energy += prob * energy;
606        }
607
608        self.history.push(QuantumAnnealingSnapshot {
609            time,
610            annealing_parameter,
611            energy_gap,
612            ground_state_probability,
613            expected_energy,
614        });
615
616        Ok(())
617    }
618
619    /// Convert computational basis state to spin configuration
620    fn state_to_spins(&self, state: usize) -> Vec<i8> {
621        (0..self.problem.num_spins)
622            .map(|i| if (state >> i) & 1 == 0 { -1 } else { 1 })
623            .collect()
624    }
625
626    /// Get annealing history
627    pub fn history(&self) -> &[QuantumAnnealingSnapshot] {
628        &self.history
629    }
630
631    /// Get best solution found
632    pub fn best_solution(&self) -> Option<&(Vec<i8>, f64)> {
633        self.best_solution.as_ref()
634    }
635
636    /// Get minimum energy gap during annealing
637    pub fn minimum_gap(&self) -> f64 {
638        self.history
639            .iter()
640            .map(|snapshot| snapshot.energy_gap)
641            .fold(f64::INFINITY, f64::min)
642    }
643
644    /// Calculate success probability (probability of finding ground state)
645    pub fn success_probability(&self) -> f64 {
646        self.history
647            .last()
648            .map(|snapshot| snapshot.ground_state_probability)
649            .unwrap_or(0.0)
650    }
651}
652
653/// Create common optimization problems for testing
654pub struct ProblemGenerator;
655
656impl ProblemGenerator {
657    /// Generate random QUBO problem
658    pub fn random_qubo(num_vars: usize, density: f64) -> QUBOProblem {
659        let mut rng = thread_rng();
660
661        let mut q_matrix = Array2::zeros((num_vars, num_vars));
662
663        for i in 0..num_vars {
664            for j in i..num_vars {
665                if rng.gen::<f64>() < density {
666                    let value = rng.gen_range(-1.0..=1.0);
667                    q_matrix[[i, j]] = value;
668                    if i != j {
669                        q_matrix[[j, i]] = value; // Symmetric
670                    }
671                }
672            }
673        }
674
675        QUBOProblem::new(q_matrix)
676            .expect("Failed to create QUBO problem in random_qubo (matrix should be square)")
677    }
678
679    /// Generate MaxCut problem on a random graph
680    pub fn max_cut(num_vertices: usize, edge_probability: f64) -> IsingProblem {
681        let mut rng = thread_rng();
682
683        let h_fields = Array1::zeros(num_vertices);
684        let mut j_couplings = Array2::zeros((num_vertices, num_vertices));
685
686        // Generate random edges with positive coupling (ferromagnetic)
687        for i in 0..num_vertices {
688            for j in i + 1..num_vertices {
689                if rng.gen::<f64>() < edge_probability {
690                    let coupling = 1.0; // Unit weight edges
691                    j_couplings[[i, j]] = coupling;
692                    j_couplings[[j, i]] = coupling;
693                }
694            }
695        }
696
697        IsingProblem::new(h_fields, j_couplings)
698            .expect("Failed to create Ising problem in max_cut (matrix dimensions should match)")
699    }
700
701    /// Generate Number Partitioning problem
702    pub fn number_partitioning(numbers: Vec<f64>) -> IsingProblem {
703        let n = numbers.len();
704        let h_fields = Array1::zeros(n);
705        let mut j_couplings = Array2::zeros((n, n));
706
707        // J_ij = numbers[i] * numbers[j]
708        for i in 0..n {
709            for j in i + 1..n {
710                let coupling = numbers[i] * numbers[j];
711                j_couplings[[i, j]] = coupling;
712                j_couplings[[j, i]] = coupling;
713            }
714        }
715
716        IsingProblem::new(h_fields, j_couplings).expect("Failed to create Ising problem in number_partitioning (matrix dimensions should match)")
717    }
718}
719
720#[cfg(test)]
721mod tests {
722    use super::*;
723
724    #[test]
725    fn test_annealing_schedules() {
726        let linear = AnnealingSchedule::Linear;
727        assert_eq!(linear.evaluate(0.0, 10.0), 0.0);
728        assert_eq!(linear.evaluate(5.0, 10.0), 0.5);
729        assert_eq!(linear.evaluate(10.0, 10.0), 1.0);
730
731        let exp = AnnealingSchedule::Exponential { rate: 1.0 };
732        assert!(exp.evaluate(0.0, 1.0) < 0.1);
733        assert!(exp.evaluate(1.0, 1.0) > 0.6);
734
735        let poly = AnnealingSchedule::Polynomial { power: 2.0 };
736        assert_eq!(poly.evaluate(10.0, 10.0), 1.0);
737        assert!(poly.evaluate(5.0, 10.0) < linear.evaluate(5.0, 10.0));
738    }
739
740    #[test]
741    fn test_qubo_problem() {
742        let mut q_matrix = Array2::zeros((2, 2));
743        q_matrix[[0, 0]] = 1.0;
744        q_matrix[[1, 1]] = 1.0;
745        q_matrix[[0, 1]] = -2.0;
746        q_matrix[[1, 0]] = -2.0;
747
748        let qubo = QUBOProblem::new(q_matrix).expect("Failed to create QUBO in test_qubo_problem");
749
750        // Test evaluations
751        // For Q = [[1, -2], [-2, 1]]:
752        // [0,0]: 0*1*0 + 0*(-2)*0 + 0*(-2)*0 + 0*1*0 = 0
753        // [1,1]: 1*1*1 + 1*(-2)*1 + 1*(-2)*1 + 1*1*1 = 1 - 2 - 2 + 1 = -2
754        // [1,0]: 1*1*1 + 1*(-2)*0 + 0*(-2)*1 + 0*1*0 = 1
755        // [0,1]: 0*1*0 + 0*(-2)*1 + 1*(-2)*0 + 1*1*1 = 1
756        assert_eq!(
757            qubo.evaluate(&[0, 0])
758                .expect("Failed to evaluate [0,0] in test_qubo_problem"),
759            0.0
760        );
761        assert_eq!(
762            qubo.evaluate(&[1, 1])
763                .expect("Failed to evaluate [1,1] in test_qubo_problem"),
764            -2.0
765        );
766        assert_eq!(
767            qubo.evaluate(&[1, 0])
768                .expect("Failed to evaluate [1,0] in test_qubo_problem"),
769            1.0
770        );
771        assert_eq!(
772            qubo.evaluate(&[0, 1])
773                .expect("Failed to evaluate [0,1] in test_qubo_problem"),
774            1.0
775        );
776    }
777
778    #[test]
779    fn test_ising_problem() {
780        let h_fields = Array1::from(vec![0.5, -0.5]);
781        let mut j_couplings = Array2::zeros((2, 2));
782        j_couplings[[0, 1]] = 1.0;
783        j_couplings[[1, 0]] = 1.0;
784
785        let ising = IsingProblem::new(h_fields, j_couplings)
786            .expect("Failed to create Ising problem in test_ising_problem");
787
788        // Test evaluations
789        // Energy = -∑_i h_i s_i - ∑_{i<j} J_{ij} s_i s_j
790        // With h = [0.5, -0.5] and J_{01} = 1.0:
791        assert_eq!(
792            ising
793                .evaluate(&[-1, -1])
794                .expect("Failed to evaluate [-1,-1] in test_ising_problem"),
795            -1.0
796        ); // -(0.5*(-1) + (-0.5)*(-1)) - 1*(-1)*(-1) = -(-0.5 + 0.5) - 1 = -1
797        assert_eq!(
798            ising
799                .evaluate(&[1, 1])
800                .expect("Failed to evaluate [1,1] in test_ising_problem"),
801            -1.0
802        ); // -(0.5*1 + (-0.5)*1) - 1*1*1 = -(0.5 - 0.5) - 1 = -1
803        assert_eq!(
804            ising
805                .evaluate(&[1, -1])
806                .expect("Failed to evaluate [1,-1] in test_ising_problem"),
807            0.0
808        ); // -(0.5*1 + (-0.5)*(-1)) - 1*1*(-1) = -(0.5 + 0.5) - (-1) = -1 + 1 = 0
809        assert_eq!(
810            ising
811                .evaluate(&[-1, 1])
812                .expect("Failed to evaluate [-1,1] in test_ising_problem"),
813            2.0
814        ); // -(0.5*(-1) + (-0.5)*1) - 1*(-1)*1 = -(-0.5 - 0.5) - (-1) = 1 + 1 = 2
815    }
816
817    #[test]
818    fn test_qubo_to_ising_conversion() {
819        let mut q_matrix = Array2::zeros((2, 2));
820        q_matrix[[0, 1]] = 1.0;
821        q_matrix[[1, 0]] = 1.0;
822
823        let qubo = QUBOProblem::new(q_matrix)
824            .expect("Failed to create QUBO in test_qubo_to_ising_conversion");
825        let ising = qubo.to_ising();
826
827        // Verify the conversion maintains the same optimal solutions
828        // QUBO: minimize x₀x₁, optimal at (0,0), (0,1), (1,0) with value 0
829        // Ising: should have corresponding optimal spin configurations
830
831        assert_eq!(ising.num_spins, 2);
832        assert!(ising.h_fields.len() == 2);
833        assert!(ising.j_couplings.dim() == (2, 2));
834    }
835
836    #[test]
837    fn test_adiabatic_computer_initialization() {
838        let h_fields = Array1::from(vec![0.0, 0.0]);
839        let j_couplings = Array2::eye(2);
840        let ising = IsingProblem::new(h_fields, j_couplings)
841            .expect("Failed to create Ising problem in test_adiabatic_computer_initialization");
842
843        let adiabatic = AdiabaticQuantumComputer::new(
844            &ising,
845            10.0, // total_time
846            0.1,  // time_step
847            AnnealingSchedule::Linear,
848        );
849
850        assert_eq!(adiabatic.num_qubits, 2);
851        assert_eq!(adiabatic.current_time, 0.0);
852        assert_eq!(adiabatic.state.len(), 4); // 2^2 = 4 states
853
854        // Initial state should be uniform superposition
855        let expected_amplitude = 1.0 / 2.0; // 1/sqrt(4)
856        for amplitude in adiabatic.state.iter() {
857            assert!((amplitude.norm() - expected_amplitude).abs() < 1e-10);
858        }
859    }
860
861    #[test]
862    fn test_adiabatic_evolution_step() {
863        let h_fields = Array1::zeros(1);
864        let j_couplings = Array2::zeros((1, 1));
865        let ising = IsingProblem::new(h_fields, j_couplings)
866            .expect("Failed to create Ising problem in test_adiabatic_evolution_step");
867
868        let mut adiabatic = AdiabaticQuantumComputer::new(
869            &ising,
870            1.0, // total_time
871            0.1, // time_step
872            AnnealingSchedule::Linear,
873        );
874
875        let initial_time = adiabatic.current_time;
876        adiabatic
877            .step()
878            .expect("Failed to step adiabatic evolution in test_adiabatic_evolution_step");
879
880        assert!(adiabatic.current_time > initial_time);
881        assert!(adiabatic.current_time <= adiabatic.total_time);
882
883        // State should remain normalized
884        let norm = adiabatic.state.iter().map(|c| c.norm_sqr()).sum::<f64>();
885        assert!((norm - 1.0).abs() < 1e-10);
886    }
887
888    #[test]
889    fn test_quantum_annealer() {
890        let problem = ProblemGenerator::max_cut(3, 0.5);
891        let mut annealer = QuantumAnnealer::new(
892            problem,
893            5.0, // total_time
894            0.1, // time_step
895            AnnealingSchedule::Linear,
896        );
897
898        let (solution, energy) = annealer
899            .optimize()
900            .expect("Failed to optimize in test_quantum_annealer");
901
902        assert_eq!(solution.len(), 3);
903        assert!(solution.iter().all(|&s| s == -1 || s == 1));
904        assert!(energy.is_finite());
905
906        // Should have recorded history
907        assert!(!annealer.history().is_empty());
908
909        // Should have a best solution
910        assert!(annealer.best_solution().is_some());
911    }
912
913    #[test]
914    fn test_problem_generators() {
915        let qubo = ProblemGenerator::random_qubo(3, 0.5);
916        assert_eq!(qubo.num_vars, 3);
917        assert_eq!(qubo.q_matrix.dim(), (3, 3));
918
919        let max_cut = ProblemGenerator::max_cut(4, 0.6);
920        assert_eq!(max_cut.num_spins, 4);
921        assert_eq!(max_cut.h_fields.len(), 4);
922        assert_eq!(max_cut.j_couplings.dim(), (4, 4));
923
924        let numbers = vec![1.0, 2.0, 3.0];
925        let num_part = ProblemGenerator::number_partitioning(numbers);
926        assert_eq!(num_part.num_spins, 3);
927    }
928
929    #[test]
930    fn test_multiple_annealing_runs() {
931        let problem = ProblemGenerator::random_qubo(2, 1.0).to_ising();
932        let mut annealer = QuantumAnnealer::new(
933            problem,
934            2.0, // total_time
935            0.2, // time_step
936            AnnealingSchedule::Linear,
937        );
938
939        let (solution, energy) = annealer
940            .optimize_multiple_runs(3)
941            .expect("Failed to optimize multiple runs in test_multiple_annealing_runs");
942
943        assert_eq!(solution.len(), 2);
944        assert!(solution.iter().all(|&s| s == -1 || s == 1));
945        assert!(energy.is_finite());
946    }
947
948    #[test]
949    fn test_annealing_parameter_progression() {
950        let problem = ProblemGenerator::max_cut(2, 1.0);
951        let mut computer = AdiabaticQuantumComputer::new(
952            &problem,
953            1.0,  // total_time
954            0.25, // time_step (4 steps total)
955            AnnealingSchedule::Linear,
956        );
957
958        // Check annealing parameter at different times
959        assert_eq!(computer.annealing_parameter(), 0.0);
960
961        computer
962            .step()
963            .expect("Failed to step (1) in test_annealing_parameter_progression");
964        assert!((computer.annealing_parameter() - 0.25).abs() < 1e-10);
965
966        computer
967            .step()
968            .expect("Failed to step (2) in test_annealing_parameter_progression");
969        assert!((computer.annealing_parameter() - 0.5).abs() < 1e-10);
970
971        computer
972            .step()
973            .expect("Failed to step (3) in test_annealing_parameter_progression");
974        assert!((computer.annealing_parameter() - 0.75).abs() < 1e-10);
975
976        computer
977            .step()
978            .expect("Failed to step (4) in test_annealing_parameter_progression");
979        assert!((computer.annealing_parameter() - 1.0).abs() < 1e-10);
980    }
981
982    #[test]
983    fn test_energy_gap_calculation() {
984        let h_fields = Array1::from(vec![1.0]);
985        let j_couplings = Array2::zeros((1, 1));
986        let ising = IsingProblem::new(h_fields, j_couplings)
987            .expect("Failed to create Ising problem in test_energy_gap_calculation");
988
989        let computer = AdiabaticQuantumComputer::new(&ising, 1.0, 0.1, AnnealingSchedule::Linear);
990
991        let gap = computer
992            .energy_gap()
993            .expect("Failed to calculate energy gap in test_energy_gap_calculation");
994        assert!(gap >= 0.0);
995    }
996
997    #[test]
998    fn test_measurement_probabilities() {
999        let h_fields = Array1::zeros(1);
1000        let j_couplings = Array2::zeros((1, 1));
1001        let ising = IsingProblem::new(h_fields, j_couplings)
1002            .expect("Failed to create Ising problem in test_measurement_probabilities");
1003
1004        let computer = AdiabaticQuantumComputer::new(&ising, 1.0, 0.1, AnnealingSchedule::Linear);
1005
1006        let probs = computer.measurement_probabilities();
1007        assert_eq!(probs.len(), 2); // 2^1 = 2 states
1008
1009        // Probabilities should sum to 1
1010        let total: f64 = probs.iter().sum();
1011        assert!((total - 1.0).abs() < 1e-10);
1012
1013        // All probabilities should be non-negative
1014        assert!(probs.iter().all(|&p| p >= 0.0));
1015    }
1016}