Skip to main content

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, Eq)]
21#[non_exhaustive]
22pub enum ProblemType {
23    /// Quadratic Unconstrained Binary Optimization
24    QUBO,
25    /// Ising spin glass model
26    Ising,
27    /// Maximum Cut problem
28    MaxCut,
29    /// Graph Coloring
30    GraphColoring,
31    /// Traveling Salesman Problem
32    TSP,
33    /// Number Partitioning
34    NumberPartitioning,
35    /// Custom problem with user-defined Hamiltonian
36    Custom,
37}
38
39/// Annealing schedule types
40#[derive(Debug, Clone, Copy)]
41#[non_exhaustive]
42pub enum AnnealingSchedule {
43    /// Linear annealing: s(t) = t/T
44    Linear,
45    /// Exponential annealing
46    Exponential { rate: f64 },
47    /// Polynomial annealing: s(t) = (t/T)^p
48    Polynomial { power: f64 },
49    /// Trigonometric annealing: s(t) = sin²(πt/2T)
50    Trigonometric,
51    /// Custom schedule with user-defined function
52    Custom(fn(f64, f64) -> f64),
53}
54
55impl AnnealingSchedule {
56    /// Evaluate the schedule at time t with total time T
57    pub fn evaluate(&self, t: f64, total_time: f64) -> f64 {
58        let s = t / total_time;
59        match self {
60            Self::Linear => s,
61            Self::Exponential { rate } => 1.0 - (-rate * s).exp(),
62            Self::Polynomial { power } => s.powf(*power),
63            Self::Trigonometric => (PI * s / 2.0).sin().powi(2),
64            Self::Custom(func) => func(t, total_time),
65        }
66    }
67
68    /// Get the derivative of the schedule
69    pub fn derivative(&self, t: f64, total_time: f64) -> f64 {
70        let dt = 1e-8;
71        let s1 = self.evaluate(t + dt, total_time);
72        let s0 = self.evaluate(t, total_time);
73        (s1 - s0) / dt
74    }
75}
76
77/// QUBO (Quadratic Unconstrained Binary Optimization) problem representation
78#[derive(Debug, Clone)]
79pub struct QUBOProblem {
80    /// Number of variables
81    pub num_vars: usize,
82    /// Q matrix: objective is x^T Q x
83    pub q_matrix: Array2<f64>,
84    /// Linear terms (optional)
85    pub linear_terms: Option<Array1<f64>>,
86    /// Constant offset
87    pub offset: f64,
88}
89
90impl QUBOProblem {
91    /// Create a new QUBO problem
92    pub fn new(q_matrix: Array2<f64>) -> QuantRS2Result<Self> {
93        let (rows, cols) = q_matrix.dim();
94        if rows != cols {
95            return Err(QuantRS2Error::InvalidInput(
96                "Q matrix must be square".to_string(),
97            ));
98        }
99
100        Ok(Self {
101            num_vars: rows,
102            q_matrix,
103            linear_terms: None,
104            offset: 0.0,
105        })
106    }
107
108    /// Add linear terms to the QUBO problem
109    #[must_use]
110    pub fn with_linear_terms(mut self, linear: Array1<f64>) -> QuantRS2Result<Self> {
111        if linear.len() != self.num_vars {
112            return Err(QuantRS2Error::InvalidInput(
113                "Linear terms size mismatch".to_string(),
114            ));
115        }
116        self.linear_terms = Some(linear);
117        Ok(self)
118    }
119
120    /// Add constant offset
121    #[must_use]
122    pub const fn with_offset(mut self, offset: f64) -> Self {
123        self.offset = offset;
124        self
125    }
126
127    /// Evaluate the QUBO objective for a binary solution
128    pub fn evaluate(&self, solution: &[u8]) -> QuantRS2Result<f64> {
129        if solution.len() != self.num_vars {
130            return Err(QuantRS2Error::InvalidInput(
131                "Solution size mismatch".to_string(),
132            ));
133        }
134
135        let mut objective = self.offset;
136
137        // Quadratic terms
138        for i in 0..self.num_vars {
139            for j in 0..self.num_vars {
140                objective += self.q_matrix[[i, j]] * solution[i] as f64 * solution[j] as f64;
141            }
142        }
143
144        // Linear terms
145        if let Some(ref linear) = self.linear_terms {
146            for i in 0..self.num_vars {
147                objective += linear[i] * solution[i] as f64;
148            }
149        }
150
151        Ok(objective)
152    }
153
154    /// Convert to Ising model (spins ∈ {-1, +1})
155    pub fn to_ising(&self) -> IsingProblem {
156        let mut h = Array1::zeros(self.num_vars);
157        let mut j = Array2::zeros((self.num_vars, self.num_vars));
158        let mut offset = self.offset;
159
160        // Transform QUBO to Ising: x_i = (1 + s_i)/2
161        for i in 0..self.num_vars {
162            // Linear terms: Q_ii * (1 + s_i)/2 + h_i * (1 + s_i)/2
163            h[i] = self.q_matrix[[i, i]] / 4.0;
164            offset += self.q_matrix[[i, i]] / 4.0;
165
166            if let Some(ref linear) = self.linear_terms {
167                h[i] += linear[i] / 2.0;
168                offset += linear[i] / 2.0;
169            }
170
171            // Quadratic terms: Q_ij * (1 + s_i)/2 * (1 + s_j)/2
172            for k in 0..self.num_vars {
173                if i != k {
174                    j[[i, k]] = self.q_matrix[[i, k]] / 4.0;
175                    h[i] += self.q_matrix[[i, k]] / 4.0;
176                    h[k] += self.q_matrix[[i, k]] / 4.0;
177                    offset += self.q_matrix[[i, k]] / 4.0;
178                }
179            }
180        }
181
182        IsingProblem {
183            num_spins: self.num_vars,
184            h_fields: h,
185            j_couplings: j,
186            offset,
187        }
188    }
189}
190
191/// Ising model problem representation
192#[derive(Debug, Clone)]
193pub struct IsingProblem {
194    /// Number of spins
195    pub num_spins: usize,
196    /// Local magnetic fields (h_i)
197    pub h_fields: Array1<f64>,
198    /// Coupling matrix (J_ij)
199    pub j_couplings: Array2<f64>,
200    /// Constant offset
201    pub offset: f64,
202}
203
204impl IsingProblem {
205    /// Create a new Ising problem
206    pub fn new(h_fields: Array1<f64>, j_couplings: Array2<f64>) -> QuantRS2Result<Self> {
207        let num_spins = h_fields.len();
208        let (rows, cols) = j_couplings.dim();
209
210        if rows != num_spins || cols != num_spins {
211            return Err(QuantRS2Error::InvalidInput(
212                "Coupling matrix size mismatch".to_string(),
213            ));
214        }
215
216        Ok(Self {
217            num_spins,
218            h_fields,
219            j_couplings,
220            offset: 0.0,
221        })
222    }
223
224    /// Evaluate the Ising energy for a spin configuration
225    pub fn evaluate(&self, spins: &[i8]) -> QuantRS2Result<f64> {
226        if spins.len() != self.num_spins {
227            return Err(QuantRS2Error::InvalidInput(
228                "Spin configuration size mismatch".to_string(),
229            ));
230        }
231
232        let mut energy = self.offset;
233
234        // Local field terms: -h_i * s_i
235        for i in 0..self.num_spins {
236            energy -= self.h_fields[i] * spins[i] as f64;
237        }
238
239        // Coupling terms: -J_ij * s_i * s_j
240        for i in 0..self.num_spins {
241            for j in i + 1..self.num_spins {
242                energy -= self.j_couplings[[i, j]] * spins[i] as f64 * spins[j] as f64;
243            }
244        }
245
246        Ok(energy)
247    }
248
249    /// Generate problem Hamiltonian as a matrix
250    pub fn hamiltonian(&self) -> Array2<Complex64> {
251        let dim = 1 << self.num_spins;
252        let mut hamiltonian = Array2::zeros((dim, dim));
253
254        for state in 0..dim {
255            let spins = self.state_to_spins(state);
256            let energy = self.evaluate(&spins).unwrap_or(0.0);
257            hamiltonian[[state, state]] = Complex64::new(energy, 0.0);
258        }
259
260        hamiltonian
261    }
262
263    /// Convert computational basis state to spin configuration
264    fn state_to_spins(&self, state: usize) -> Vec<i8> {
265        (0..self.num_spins)
266            .map(|i| if (state >> i) & 1 == 0 { -1 } else { 1 })
267            .collect()
268    }
269
270    /// Convert spin configuration to computational basis state
271    fn spins_to_state(spins: &[i8]) -> usize {
272        spins
273            .iter()
274            .enumerate()
275            .fold(0, |acc, (i, &spin)| acc | usize::from(spin == 1) << i)
276    }
277}
278
279/// Adiabatic quantum computer simulator
280pub struct AdiabaticQuantumComputer {
281    /// Problem Hamiltonian (H_P)
282    problem_hamiltonian: Array2<Complex64>,
283    /// Initial Hamiltonian (H_0) - typically transverse field
284    initial_hamiltonian: Array2<Complex64>,
285    /// Current quantum state
286    state: Array1<Complex64>,
287    /// Number of qubits/spins
288    num_qubits: usize,
289    /// Total annealing time
290    total_time: f64,
291    /// Current time
292    current_time: f64,
293    /// Time step for evolution
294    time_step: f64,
295    /// Annealing schedule
296    schedule: AnnealingSchedule,
297}
298
299impl AdiabaticQuantumComputer {
300    /// Create a new adiabatic quantum computer
301    pub fn new(
302        problem: &IsingProblem,
303        total_time: f64,
304        time_step: f64,
305        schedule: AnnealingSchedule,
306    ) -> Self {
307        let num_qubits = problem.num_spins;
308        let dim = 1 << num_qubits;
309
310        // Problem Hamiltonian
311        let problem_hamiltonian = problem.hamiltonian();
312
313        // Initial Hamiltonian: H_0 = -∑_i σ_x^i (transverse field)
314        let mut initial_hamiltonian = Array2::zeros((dim, dim));
315        for i in 0..num_qubits {
316            let pauli_x = Self::pauli_x_tensor(i, num_qubits);
317            initial_hamiltonian = initial_hamiltonian + pauli_x;
318        }
319        initial_hamiltonian = initial_hamiltonian.mapv(|x: Complex64| -x);
320
321        // Initial state: uniform superposition (ground state of H_0)
322        let mut state = Array1::zeros(dim);
323        let amplitude = Complex64::new(1.0 / (dim as f64).sqrt(), 0.0);
324        state.fill(amplitude);
325
326        Self {
327            problem_hamiltonian,
328            initial_hamiltonian,
329            state,
330            num_qubits,
331            total_time,
332            current_time: 0.0,
333            time_step,
334            schedule,
335        }
336    }
337
338    /// Generate Pauli-X tensor product for qubit i
339    fn pauli_x_tensor(target_qubit: usize, num_qubits: usize) -> Array2<Complex64> {
340        let dim = 1 << num_qubits;
341        let mut result = Array2::zeros((dim, dim));
342
343        for state in 0..dim {
344            // Flip bit at target_qubit position
345            let flipped_state = state ^ (1 << target_qubit);
346            result[[state, flipped_state]] = Complex64::new(1.0, 0.0);
347        }
348
349        result
350    }
351
352    /// Get the current Hamiltonian H(t) = (1-s(t))H_0 + s(t)H_P
353    pub fn current_hamiltonian(&self) -> Array2<Complex64> {
354        let s = self.schedule.evaluate(self.current_time, self.total_time);
355        let one_minus_s = 1.0 - s;
356
357        self.initial_hamiltonian.mapv(|x| x * one_minus_s)
358            + self.problem_hamiltonian.mapv(|x| x * s)
359    }
360
361    /// Perform one time step of adiabatic evolution
362    pub fn step(&mut self) -> QuantRS2Result<()> {
363        if self.current_time >= self.total_time {
364            return Ok(());
365        }
366
367        let hamiltonian = self.current_hamiltonian();
368
369        // Apply time evolution: |ψ(t+dt)⟩ = exp(-iH(t)dt)|ψ(t)⟩
370        // Using first-order approximation: exp(-iHdt) ≈ I - iHdt
371        let dt = self.time_step.min(self.total_time - self.current_time);
372        let evolution_operator = Self::compute_evolution_operator(&hamiltonian, dt)?;
373
374        self.state = evolution_operator.dot(&self.state);
375        self.current_time += dt;
376
377        // Normalize state
378        let norm = self.state.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
379        if norm > 0.0 {
380            self.state.mapv_inplace(|c| c / norm);
381        }
382
383        Ok(())
384    }
385
386    /// Compute evolution operator exp(-iHdt) using matrix exponentiation
387    fn compute_evolution_operator(
388        hamiltonian: &Array2<Complex64>,
389        dt: f64,
390    ) -> QuantRS2Result<Array2<Complex64>> {
391        let dim = hamiltonian.nrows();
392        let mut evolution = Array2::eye(dim);
393
394        // Use series expansion: exp(-iHdt) = I - iHdt - (Hdt)²/2! + i(Hdt)³/3! + ...
395        let mut term = Array2::eye(dim);
396        let h_dt = hamiltonian.mapv(|h| Complex64::new(0.0, -dt) * h);
397
398        for n in 1..20 {
399            // Truncate series at 20 terms
400            term = term.dot(&h_dt);
401            let coefficient = 1.0 / (1..=n).fold(1.0, |acc, i| acc * i as f64);
402            evolution = evolution + term.mapv(|t| t * coefficient);
403
404            // Check convergence
405            let term_norm = term.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
406            if term_norm * coefficient < 1e-12 {
407                break;
408            }
409        }
410
411        Ok(evolution)
412    }
413
414    /// Run the complete adiabatic evolution
415    pub fn run(&mut self) -> QuantRS2Result<()> {
416        while self.current_time < self.total_time {
417            self.step()?;
418        }
419        Ok(())
420    }
421
422    /// Get measurement probabilities in computational basis
423    pub fn measurement_probabilities(&self) -> Vec<f64> {
424        self.state.iter().map(|c| c.norm_sqr()).collect()
425    }
426
427    /// Sample a measurement result
428    pub fn measure(&self) -> usize {
429        let mut rng = thread_rng();
430        let probs = self.measurement_probabilities();
431
432        let random_value: f64 = rng.random();
433        let mut cumulative = 0.0;
434
435        for (state, prob) in probs.iter().enumerate() {
436            cumulative += prob;
437            if random_value <= cumulative {
438                return state;
439            }
440        }
441
442        probs.len() - 1 // Fallback
443    }
444
445    /// Get the instantaneous energy gap
446    pub fn energy_gap(&self) -> QuantRS2Result<f64> {
447        let hamiltonian = self.current_hamiltonian();
448
449        // For small systems, compute eigenvalues directly
450        if self.num_qubits <= 8 {
451            let eigenvalues = Self::compute_eigenvalues(&hamiltonian)?;
452            let ground_energy = eigenvalues[0];
453            let first_excited = eigenvalues[1];
454            Ok(first_excited - ground_energy)
455        } else {
456            // For larger systems, use approximation
457            Ok(self.estimate_gap(&hamiltonian))
458        }
459    }
460
461    /// Compute eigenvalues of a Hermitian matrix using inverse power iteration with deflation.
462    ///
463    /// The Hamiltonian H is Hermitian (H† = H), so all eigenvalues are real.
464    /// We use inverse power iteration (shift-and-invert) to find the lowest eigenvalues:
465    ///   1. Estimate shift σ ≈ tr(H)/n (near the ground state energy).
466    ///   2. Solve (H - σI)v = b iteratively (Gaussian elimination with partial pivoting).
467    ///   3. Rayleigh quotient λ ≈ v†Hv / v†v refines each eigenvalue.
468    ///   4. Deflate found eigenvectors to find subsequent eigenvalues.
469    ///
470    /// Falls back to diagonal extraction for degenerate or trivial cases.
471    fn compute_eigenvalues(hamiltonian: &Array2<Complex64>) -> QuantRS2Result<Vec<f64>> {
472        let dim = hamiltonian.nrows();
473        if dim == 0 {
474            return Ok(vec![]);
475        }
476        if dim == 1 {
477            return Ok(vec![hamiltonian[[0, 0]].re]);
478        }
479
480        // Compute trace / n as initial shift estimate
481        let trace: f64 = (0..dim).map(|i| hamiltonian[[i, i]].re).sum();
482        let trace_mean = trace / dim as f64;
483
484        // Estimate Frobenius norm for shift adjustment
485        let frob_sq: f64 = hamiltonian.iter().map(|z| z.norm_sqr()).sum();
486        let frob = frob_sq.sqrt();
487        // Shift below the expected ground state
488        let sigma = trace_mean - frob / (dim as f64).sqrt();
489
490        // We seek at least min(dim, 2) eigenvalues via inverse power iteration + deflation
491        let num_wanted = dim.min(dim); // all eigenvalues
492        let mut eigenvalues: Vec<f64> = Vec::with_capacity(num_wanted);
493        // Collected orthonormal eigenvectors for deflation
494        let mut found_vecs: Vec<Vec<Complex64>> = Vec::new();
495
496        for ev_idx in 0..num_wanted {
497            // Shift for this iteration: use sigma slightly below previous found EV
498            let current_shift = if ev_idx == 0 {
499                sigma
500            } else {
501                eigenvalues[ev_idx - 1] - 0.1 * (1.0 + eigenvalues[ev_idx - 1].abs())
502            };
503
504            // Build (H - σI), deflated by already-found eigenvectors
505            // Start with a random-ish vector (deterministic: alternating ±1/√dim)
506            let inv_sqrt_dim = 1.0 / (dim as f64).sqrt();
507            let mut v: Vec<Complex64> = (0..dim)
508                .map(|i| {
509                    if i % 2 == 0 {
510                        Complex64::new(inv_sqrt_dim, 0.0)
511                    } else {
512                        Complex64::new(-inv_sqrt_dim, 0.0)
513                    }
514                })
515                .collect();
516
517            // Orthogonalise initial vector against already-found eigenvectors
518            Self::orthogonalise_against(&mut v, &found_vecs);
519            Self::normalize_vec(&mut v);
520
521            let mut rayleigh = Self::rayleigh_quotient(hamiltonian, &v);
522
523            for _iter in 0..150 {
524                // Solve (H - current_shift·I) w = v using Gaussian elimination
525                let shifted = Self::build_shifted_matrix(hamiltonian, current_shift);
526                let w = match Self::gaussian_elimination_complex(&shifted, &v) {
527                    Ok(sol) => sol,
528                    Err(_) => {
529                        // Matrix is singular (shift is an eigenvalue): v is already the eigenvector
530                        break;
531                    }
532                };
533
534                // Orthogonalise w against found eigenvectors before normalising
535                let mut w = w;
536                Self::orthogonalise_against(&mut w, &found_vecs);
537                let norm_w = Self::vec_norm(&w);
538                if norm_w < 1e-14 {
539                    break;
540                }
541                for x in &mut w {
542                    *x = *x / norm_w;
543                }
544
545                let new_rayleigh = Self::rayleigh_quotient(hamiltonian, &w);
546                let delta = (new_rayleigh - rayleigh).abs();
547                v = w;
548                rayleigh = new_rayleigh;
549
550                if delta < 1e-10 {
551                    break;
552                }
553            }
554
555            eigenvalues.push(rayleigh);
556            // Store the converged eigenvector for deflation
557            found_vecs.push(v);
558        }
559
560        eigenvalues.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
561        Ok(eigenvalues)
562    }
563
564    /// Build (H - σI) as a matrix of Complex64
565    fn build_shifted_matrix(h: &Array2<Complex64>, sigma: f64) -> Array2<Complex64> {
566        let dim = h.nrows();
567        let mut m = h.to_owned();
568        for i in 0..dim {
569            m[[i, i]] = m[[i, i]] - Complex64::new(sigma, 0.0);
570        }
571        m
572    }
573
574    /// Rayleigh quotient λ = v†Hv / v†v for Hermitian H (result is real)
575    fn rayleigh_quotient(h: &Array2<Complex64>, v: &[Complex64]) -> f64 {
576        let dim = v.len();
577        let mut hv = vec![Complex64::new(0.0, 0.0); dim];
578        for i in 0..dim {
579            for j in 0..dim {
580                hv[i] = hv[i] + h[[i, j]] * v[j];
581            }
582        }
583        // v†Hv
584        let vhv: Complex64 = v
585            .iter()
586            .zip(hv.iter())
587            .map(|(vi, hvi)| vi.conj() * hvi)
588            .sum();
589        // v†v
590        let vv: f64 = v.iter().map(|vi| vi.norm_sqr()).sum();
591        if vv < 1e-30 {
592            0.0
593        } else {
594            vhv.re / vv
595        }
596    }
597
598    /// Orthogonalise v against a list of orthonormal vectors (Gram-Schmidt)
599    fn orthogonalise_against(v: &mut Vec<Complex64>, basis: &[Vec<Complex64>]) {
600        for u in basis {
601            // proj = u†v
602            let proj: Complex64 = u.iter().zip(v.iter()).map(|(ui, vi)| ui.conj() * vi).sum();
603            for (vi, ui) in v.iter_mut().zip(u.iter()) {
604                *vi = *vi - proj * ui;
605            }
606        }
607    }
608
609    /// Euclidean norm of a complex vector
610    fn vec_norm(v: &[Complex64]) -> f64 {
611        v.iter().map(|z| z.norm_sqr()).sum::<f64>().sqrt()
612    }
613
614    /// Normalise a complex vector in place; no-op if norm is tiny
615    fn normalize_vec(v: &mut Vec<Complex64>) {
616        let n = Self::vec_norm(v);
617        if n > 1e-14 {
618            for x in v.iter_mut() {
619                *x = *x / n;
620            }
621        }
622    }
623
624    /// Solve A·x = b using Gaussian elimination with partial (column) pivoting.
625    /// Returns `Err` if the matrix is numerically singular.
626    fn gaussian_elimination_complex(
627        a: &Array2<Complex64>,
628        b: &[Complex64],
629    ) -> QuantRS2Result<Vec<Complex64>> {
630        let n = a.nrows();
631        // Build augmented matrix [A | b]
632        let mut aug: Vec<Vec<Complex64>> = (0..n)
633            .map(|i| {
634                let mut row: Vec<Complex64> = (0..n).map(|j| a[[i, j]]).collect();
635                row.push(b[i]);
636                row
637            })
638            .collect();
639
640        for col in 0..n {
641            // Partial pivoting: find row with max magnitude in column `col`
642            let max_row = (col..n)
643                .max_by(|&r1, &r2| {
644                    aug[r1][col]
645                        .norm()
646                        .partial_cmp(&aug[r2][col].norm())
647                        .unwrap_or(std::cmp::Ordering::Equal)
648                })
649                .unwrap_or(col);
650
651            aug.swap(col, max_row);
652
653            let pivot = aug[col][col];
654            if pivot.norm() < 1e-14 {
655                return Err(QuantRS2Error::ComputationError(
656                    "Singular matrix in Gaussian elimination".to_string(),
657                ));
658            }
659
660            // Eliminate below
661            for row in (col + 1)..n {
662                let factor = aug[row][col] / pivot;
663                for j in col..=n {
664                    let sub = factor * aug[col][j];
665                    aug[row][j] = aug[row][j] - sub;
666                }
667            }
668        }
669
670        // Back substitution
671        let mut x = vec![Complex64::new(0.0, 0.0); n];
672        for i in (0..n).rev() {
673            let mut sum = aug[i][n];
674            for j in (i + 1)..n {
675                sum = sum - aug[i][j] * x[j];
676            }
677            if aug[i][i].norm() < 1e-14 {
678                return Err(QuantRS2Error::ComputationError(
679                    "Singular matrix in back substitution".to_string(),
680                ));
681            }
682            x[i] = sum / aug[i][i];
683        }
684
685        Ok(x)
686    }
687
688    /// Estimate energy gap using power iteration
689    fn estimate_gap(&self, _hamiltonian: &Array2<Complex64>) -> f64 {
690        // Simplified estimation - would use proper numerical methods in practice
691        let s = self.schedule.evaluate(self.current_time, self.total_time);
692        // Gap typically closes as sin(πs) near the end of annealing
693        (PI * s).sin() * 0.1 // Rough approximation
694    }
695
696    /// Check if adiabatic condition is satisfied
697    pub fn adiabatic_condition_satisfied(&self) -> QuantRS2Result<bool> {
698        let gap = self.energy_gap()?;
699        let s_dot = self.schedule.derivative(self.current_time, self.total_time);
700
701        // Adiabatic condition: |⟨1|dH/dt|0⟩| << Δ²
702        // Simplified check: gap should be much larger than evolution rate
703        Ok(gap > 10.0 * s_dot.abs())
704    }
705
706    /// Reset to initial state
707    pub fn reset(&mut self) {
708        let dim = 1 << self.num_qubits;
709        let amplitude = Complex64::new(1.0 / (dim as f64).sqrt(), 0.0);
710        self.state.fill(amplitude);
711        self.current_time = 0.0;
712    }
713
714    /// Get current state vector
715    pub const fn state(&self) -> &Array1<Complex64> {
716        &self.state
717    }
718
719    /// Get current annealing parameter s(t)
720    pub fn annealing_parameter(&self) -> f64 {
721        self.schedule.evaluate(self.current_time, self.total_time)
722    }
723
724    /// Get progress as percentage
725    pub fn progress(&self) -> f64 {
726        (self.current_time / self.total_time).min(1.0) * 100.0
727    }
728}
729
730/// Quantum annealing optimizer for solving optimization problems
731pub struct QuantumAnnealer {
732    computer: AdiabaticQuantumComputer,
733    problem: IsingProblem,
734    best_solution: Option<(Vec<i8>, f64)>,
735    history: Vec<QuantumAnnealingSnapshot>,
736}
737
738/// Snapshot of quantum annealing state at a point in time
739#[derive(Debug, Clone)]
740pub struct QuantumAnnealingSnapshot {
741    pub time: f64,
742    pub annealing_parameter: f64,
743    pub energy_gap: f64,
744    pub ground_state_probability: f64,
745    pub expected_energy: f64,
746}
747
748impl QuantumAnnealer {
749    /// Create a new quantum annealer
750    pub fn new(
751        problem: IsingProblem,
752        total_time: f64,
753        time_step: f64,
754        schedule: AnnealingSchedule,
755    ) -> Self {
756        let computer = AdiabaticQuantumComputer::new(&problem, total_time, time_step, schedule);
757
758        Self {
759            computer,
760            problem,
761            best_solution: None,
762            history: Vec::new(),
763        }
764    }
765
766    /// Run quantum annealing optimization
767    pub fn optimize(&mut self) -> QuantRS2Result<(Vec<i8>, f64)> {
768        self.computer.reset();
769        self.history.clear();
770
771        // Record initial state
772        self.record_snapshot()?;
773
774        // Run adiabatic evolution
775        while self.computer.current_time < self.computer.total_time {
776            self.computer.step()?;
777
778            // Record snapshots at regular intervals
779            if self.history.len() % 10 == 0 {
780                self.record_snapshot()?;
781            }
782        }
783
784        // Final measurement and solution extraction
785        let measurement = self.computer.measure();
786        let spins = self.state_to_spins(measurement);
787        let energy = self.problem.evaluate(&spins)?;
788
789        self.best_solution = Some((spins.clone(), energy));
790
791        Ok((spins, energy))
792    }
793
794    /// Run multiple annealing runs and return the best solution
795    pub fn optimize_multiple_runs(&mut self, num_runs: usize) -> QuantRS2Result<(Vec<i8>, f64)> {
796        let mut best_energy = f64::INFINITY;
797        let mut best_spins = vec![];
798
799        for _ in 0..num_runs {
800            let (spins, energy) = self.optimize()?;
801            if energy < best_energy {
802                best_energy = energy;
803                best_spins = spins;
804            }
805        }
806
807        Ok((best_spins, best_energy))
808    }
809
810    /// Record a snapshot of the current annealing state
811    fn record_snapshot(&mut self) -> QuantRS2Result<()> {
812        let time = self.computer.current_time;
813        let annealing_parameter = self.computer.annealing_parameter();
814        let energy_gap = self.computer.energy_gap()?;
815
816        // Calculate ground state probability and expected energy
817        let probs = self.computer.measurement_probabilities();
818        let ground_state_probability = probs[0]; // Assuming ground state is first
819
820        let mut expected_energy = 0.0;
821        for (state_idx, &prob) in probs.iter().enumerate() {
822            let spins = self.state_to_spins(state_idx);
823            let energy = self.problem.evaluate(&spins).unwrap_or(0.0);
824            expected_energy += prob * energy;
825        }
826
827        self.history.push(QuantumAnnealingSnapshot {
828            time,
829            annealing_parameter,
830            energy_gap,
831            ground_state_probability,
832            expected_energy,
833        });
834
835        Ok(())
836    }
837
838    /// Convert computational basis state to spin configuration
839    fn state_to_spins(&self, state: usize) -> Vec<i8> {
840        (0..self.problem.num_spins)
841            .map(|i| if (state >> i) & 1 == 0 { -1 } else { 1 })
842            .collect()
843    }
844
845    /// Get annealing history
846    pub fn history(&self) -> &[QuantumAnnealingSnapshot] {
847        &self.history
848    }
849
850    /// Get best solution found
851    pub const fn best_solution(&self) -> Option<&(Vec<i8>, f64)> {
852        self.best_solution.as_ref()
853    }
854
855    /// Get minimum energy gap during annealing
856    pub fn minimum_gap(&self) -> f64 {
857        self.history
858            .iter()
859            .map(|snapshot| snapshot.energy_gap)
860            .fold(f64::INFINITY, f64::min)
861    }
862
863    /// Calculate success probability (probability of finding ground state)
864    pub fn success_probability(&self) -> f64 {
865        self.history
866            .last()
867            .map_or(0.0, |snapshot| snapshot.ground_state_probability)
868    }
869}
870
871/// Create common optimization problems for testing
872pub struct ProblemGenerator;
873
874impl ProblemGenerator {
875    /// Generate random QUBO problem
876    pub fn random_qubo(num_vars: usize, density: f64) -> QUBOProblem {
877        let mut rng = thread_rng();
878
879        let mut q_matrix = Array2::zeros((num_vars, num_vars));
880
881        for i in 0..num_vars {
882            for j in i..num_vars {
883                if rng.random::<f64>() < density {
884                    let value = rng.random_range(-1.0..=1.0);
885                    q_matrix[[i, j]] = value;
886                    if i != j {
887                        q_matrix[[j, i]] = value; // Symmetric
888                    }
889                }
890            }
891        }
892
893        QUBOProblem::new(q_matrix)
894            .expect("Failed to create QUBO problem in random_qubo (matrix should be square)")
895    }
896
897    /// Generate MaxCut problem on a random graph
898    pub fn max_cut(num_vertices: usize, edge_probability: f64) -> IsingProblem {
899        let mut rng = thread_rng();
900
901        let h_fields = Array1::zeros(num_vertices);
902        let mut j_couplings = Array2::zeros((num_vertices, num_vertices));
903
904        // Generate random edges with positive coupling (ferromagnetic)
905        for i in 0..num_vertices {
906            for j in i + 1..num_vertices {
907                if rng.random::<f64>() < edge_probability {
908                    let coupling = 1.0; // Unit weight edges
909                    j_couplings[[i, j]] = coupling;
910                    j_couplings[[j, i]] = coupling;
911                }
912            }
913        }
914
915        IsingProblem::new(h_fields, j_couplings)
916            .expect("Failed to create Ising problem in max_cut (matrix dimensions should match)")
917    }
918
919    /// Generate Number Partitioning problem
920    pub fn number_partitioning(numbers: Vec<f64>) -> IsingProblem {
921        let n = numbers.len();
922        let h_fields = Array1::zeros(n);
923        let mut j_couplings = Array2::zeros((n, n));
924
925        // J_ij = numbers[i] * numbers[j]
926        for i in 0..n {
927            for j in i + 1..n {
928                let coupling = numbers[i] * numbers[j];
929                j_couplings[[i, j]] = coupling;
930                j_couplings[[j, i]] = coupling;
931            }
932        }
933
934        IsingProblem::new(h_fields, j_couplings).expect("Failed to create Ising problem in number_partitioning (matrix dimensions should match)")
935    }
936}
937
938#[cfg(test)]
939mod tests {
940    use super::*;
941
942    #[test]
943    fn test_annealing_schedules() {
944        let linear = AnnealingSchedule::Linear;
945        assert_eq!(linear.evaluate(0.0, 10.0), 0.0);
946        assert_eq!(linear.evaluate(5.0, 10.0), 0.5);
947        assert_eq!(linear.evaluate(10.0, 10.0), 1.0);
948
949        let exp = AnnealingSchedule::Exponential { rate: 1.0 };
950        assert!(exp.evaluate(0.0, 1.0) < 0.1);
951        assert!(exp.evaluate(1.0, 1.0) > 0.6);
952
953        let poly = AnnealingSchedule::Polynomial { power: 2.0 };
954        assert_eq!(poly.evaluate(10.0, 10.0), 1.0);
955        assert!(poly.evaluate(5.0, 10.0) < linear.evaluate(5.0, 10.0));
956    }
957
958    #[test]
959    fn test_qubo_problem() {
960        let mut q_matrix = Array2::zeros((2, 2));
961        q_matrix[[0, 0]] = 1.0;
962        q_matrix[[1, 1]] = 1.0;
963        q_matrix[[0, 1]] = -2.0;
964        q_matrix[[1, 0]] = -2.0;
965
966        let qubo = QUBOProblem::new(q_matrix).expect("Failed to create QUBO in test_qubo_problem");
967
968        // Test evaluations
969        // For Q = [[1, -2], [-2, 1]]:
970        // [0,0]: 0*1*0 + 0*(-2)*0 + 0*(-2)*0 + 0*1*0 = 0
971        // [1,1]: 1*1*1 + 1*(-2)*1 + 1*(-2)*1 + 1*1*1 = 1 - 2 - 2 + 1 = -2
972        // [1,0]: 1*1*1 + 1*(-2)*0 + 0*(-2)*1 + 0*1*0 = 1
973        // [0,1]: 0*1*0 + 0*(-2)*1 + 1*(-2)*0 + 1*1*1 = 1
974        assert_eq!(
975            qubo.evaluate(&[0, 0])
976                .expect("Failed to evaluate [0,0] in test_qubo_problem"),
977            0.0
978        );
979        assert_eq!(
980            qubo.evaluate(&[1, 1])
981                .expect("Failed to evaluate [1,1] in test_qubo_problem"),
982            -2.0
983        );
984        assert_eq!(
985            qubo.evaluate(&[1, 0])
986                .expect("Failed to evaluate [1,0] in test_qubo_problem"),
987            1.0
988        );
989        assert_eq!(
990            qubo.evaluate(&[0, 1])
991                .expect("Failed to evaluate [0,1] in test_qubo_problem"),
992            1.0
993        );
994    }
995
996    #[test]
997    fn test_ising_problem() {
998        let h_fields = Array1::from(vec![0.5, -0.5]);
999        let mut j_couplings = Array2::zeros((2, 2));
1000        j_couplings[[0, 1]] = 1.0;
1001        j_couplings[[1, 0]] = 1.0;
1002
1003        let ising = IsingProblem::new(h_fields, j_couplings)
1004            .expect("Failed to create Ising problem in test_ising_problem");
1005
1006        // Test evaluations
1007        // Energy = -∑_i h_i s_i - ∑_{i<j} J_{ij} s_i s_j
1008        // With h = [0.5, -0.5] and J_{01} = 1.0:
1009        assert_eq!(
1010            ising
1011                .evaluate(&[-1, -1])
1012                .expect("Failed to evaluate [-1,-1] in test_ising_problem"),
1013            -1.0
1014        ); // -(0.5*(-1) + (-0.5)*(-1)) - 1*(-1)*(-1) = -(-0.5 + 0.5) - 1 = -1
1015        assert_eq!(
1016            ising
1017                .evaluate(&[1, 1])
1018                .expect("Failed to evaluate [1,1] in test_ising_problem"),
1019            -1.0
1020        ); // -(0.5*1 + (-0.5)*1) - 1*1*1 = -(0.5 - 0.5) - 1 = -1
1021        assert_eq!(
1022            ising
1023                .evaluate(&[1, -1])
1024                .expect("Failed to evaluate [1,-1] in test_ising_problem"),
1025            0.0
1026        ); // -(0.5*1 + (-0.5)*(-1)) - 1*1*(-1) = -(0.5 + 0.5) - (-1) = -1 + 1 = 0
1027        assert_eq!(
1028            ising
1029                .evaluate(&[-1, 1])
1030                .expect("Failed to evaluate [-1,1] in test_ising_problem"),
1031            2.0
1032        ); // -(0.5*(-1) + (-0.5)*1) - 1*(-1)*1 = -(-0.5 - 0.5) - (-1) = 1 + 1 = 2
1033    }
1034
1035    #[test]
1036    fn test_qubo_to_ising_conversion() {
1037        let mut q_matrix = Array2::zeros((2, 2));
1038        q_matrix[[0, 1]] = 1.0;
1039        q_matrix[[1, 0]] = 1.0;
1040
1041        let qubo = QUBOProblem::new(q_matrix)
1042            .expect("Failed to create QUBO in test_qubo_to_ising_conversion");
1043        let ising = qubo.to_ising();
1044
1045        // Verify the conversion maintains the same optimal solutions
1046        // QUBO: minimize x₀x₁, optimal at (0,0), (0,1), (1,0) with value 0
1047        // Ising: should have corresponding optimal spin configurations
1048
1049        assert_eq!(ising.num_spins, 2);
1050        assert!(ising.h_fields.len() == 2);
1051        assert!(ising.j_couplings.dim() == (2, 2));
1052    }
1053
1054    #[test]
1055    fn test_adiabatic_computer_initialization() {
1056        let h_fields = Array1::from(vec![0.0, 0.0]);
1057        let j_couplings = Array2::eye(2);
1058        let ising = IsingProblem::new(h_fields, j_couplings)
1059            .expect("Failed to create Ising problem in test_adiabatic_computer_initialization");
1060
1061        let adiabatic = AdiabaticQuantumComputer::new(
1062            &ising,
1063            10.0, // total_time
1064            0.1,  // time_step
1065            AnnealingSchedule::Linear,
1066        );
1067
1068        assert_eq!(adiabatic.num_qubits, 2);
1069        assert_eq!(adiabatic.current_time, 0.0);
1070        assert_eq!(adiabatic.state.len(), 4); // 2^2 = 4 states
1071
1072        // Initial state should be uniform superposition
1073        let expected_amplitude = 1.0 / 2.0; // 1/sqrt(4)
1074        for amplitude in adiabatic.state.iter() {
1075            assert!((amplitude.norm() - expected_amplitude).abs() < 1e-10);
1076        }
1077    }
1078
1079    #[test]
1080    fn test_adiabatic_evolution_step() {
1081        let h_fields = Array1::zeros(1);
1082        let j_couplings = Array2::zeros((1, 1));
1083        let ising = IsingProblem::new(h_fields, j_couplings)
1084            .expect("Failed to create Ising problem in test_adiabatic_evolution_step");
1085
1086        let mut adiabatic = AdiabaticQuantumComputer::new(
1087            &ising,
1088            1.0, // total_time
1089            0.1, // time_step
1090            AnnealingSchedule::Linear,
1091        );
1092
1093        let initial_time = adiabatic.current_time;
1094        adiabatic
1095            .step()
1096            .expect("Failed to step adiabatic evolution in test_adiabatic_evolution_step");
1097
1098        assert!(adiabatic.current_time > initial_time);
1099        assert!(adiabatic.current_time <= adiabatic.total_time);
1100
1101        // State should remain normalized
1102        let norm = adiabatic.state.iter().map(|c| c.norm_sqr()).sum::<f64>();
1103        assert!((norm - 1.0).abs() < 1e-10);
1104    }
1105
1106    #[test]
1107    fn test_quantum_annealer() {
1108        let problem = ProblemGenerator::max_cut(3, 0.5);
1109        let mut annealer = QuantumAnnealer::new(
1110            problem,
1111            5.0, // total_time
1112            0.1, // time_step
1113            AnnealingSchedule::Linear,
1114        );
1115
1116        let (solution, energy) = annealer
1117            .optimize()
1118            .expect("Failed to optimize in test_quantum_annealer");
1119
1120        assert_eq!(solution.len(), 3);
1121        assert!(solution.iter().all(|&s| s == -1 || s == 1));
1122        assert!(energy.is_finite());
1123
1124        // Should have recorded history
1125        assert!(!annealer.history().is_empty());
1126
1127        // Should have a best solution
1128        assert!(annealer.best_solution().is_some());
1129    }
1130
1131    #[test]
1132    fn test_problem_generators() {
1133        let qubo = ProblemGenerator::random_qubo(3, 0.5);
1134        assert_eq!(qubo.num_vars, 3);
1135        assert_eq!(qubo.q_matrix.dim(), (3, 3));
1136
1137        let max_cut = ProblemGenerator::max_cut(4, 0.6);
1138        assert_eq!(max_cut.num_spins, 4);
1139        assert_eq!(max_cut.h_fields.len(), 4);
1140        assert_eq!(max_cut.j_couplings.dim(), (4, 4));
1141
1142        let numbers = vec![1.0, 2.0, 3.0];
1143        let num_part = ProblemGenerator::number_partitioning(numbers);
1144        assert_eq!(num_part.num_spins, 3);
1145    }
1146
1147    #[test]
1148    fn test_multiple_annealing_runs() {
1149        let problem = ProblemGenerator::random_qubo(2, 1.0).to_ising();
1150        let mut annealer = QuantumAnnealer::new(
1151            problem,
1152            2.0, // total_time
1153            0.2, // time_step
1154            AnnealingSchedule::Linear,
1155        );
1156
1157        let (solution, energy) = annealer
1158            .optimize_multiple_runs(3)
1159            .expect("Failed to optimize multiple runs in test_multiple_annealing_runs");
1160
1161        assert_eq!(solution.len(), 2);
1162        assert!(solution.iter().all(|&s| s == -1 || s == 1));
1163        assert!(energy.is_finite());
1164    }
1165
1166    #[test]
1167    fn test_annealing_parameter_progression() {
1168        let problem = ProblemGenerator::max_cut(2, 1.0);
1169        let mut computer = AdiabaticQuantumComputer::new(
1170            &problem,
1171            1.0,  // total_time
1172            0.25, // time_step (4 steps total)
1173            AnnealingSchedule::Linear,
1174        );
1175
1176        // Check annealing parameter at different times
1177        assert_eq!(computer.annealing_parameter(), 0.0);
1178
1179        computer
1180            .step()
1181            .expect("Failed to step (1) in test_annealing_parameter_progression");
1182        assert!((computer.annealing_parameter() - 0.25).abs() < 1e-10);
1183
1184        computer
1185            .step()
1186            .expect("Failed to step (2) in test_annealing_parameter_progression");
1187        assert!((computer.annealing_parameter() - 0.5).abs() < 1e-10);
1188
1189        computer
1190            .step()
1191            .expect("Failed to step (3) in test_annealing_parameter_progression");
1192        assert!((computer.annealing_parameter() - 0.75).abs() < 1e-10);
1193
1194        computer
1195            .step()
1196            .expect("Failed to step (4) in test_annealing_parameter_progression");
1197        assert!((computer.annealing_parameter() - 1.0).abs() < 1e-10);
1198    }
1199
1200    #[test]
1201    fn test_energy_gap_calculation() {
1202        let h_fields = Array1::from(vec![1.0]);
1203        let j_couplings = Array2::zeros((1, 1));
1204        let ising = IsingProblem::new(h_fields, j_couplings)
1205            .expect("Failed to create Ising problem in test_energy_gap_calculation");
1206
1207        let computer = AdiabaticQuantumComputer::new(&ising, 1.0, 0.1, AnnealingSchedule::Linear);
1208
1209        let gap = computer
1210            .energy_gap()
1211            .expect("Failed to calculate energy gap in test_energy_gap_calculation");
1212        assert!(gap >= 0.0);
1213    }
1214
1215    #[test]
1216    fn test_measurement_probabilities() {
1217        let h_fields = Array1::zeros(1);
1218        let j_couplings = Array2::zeros((1, 1));
1219        let ising = IsingProblem::new(h_fields, j_couplings)
1220            .expect("Failed to create Ising problem in test_measurement_probabilities");
1221
1222        let computer = AdiabaticQuantumComputer::new(&ising, 1.0, 0.1, AnnealingSchedule::Linear);
1223
1224        let probs = computer.measurement_probabilities();
1225        assert_eq!(probs.len(), 2); // 2^1 = 2 states
1226
1227        // Probabilities should sum to 1
1228        let total: f64 = probs.iter().sum();
1229        assert!((total - 1.0).abs() < 1e-10);
1230
1231        // All probabilities should be non-negative
1232        assert!(probs.iter().all(|&p| p >= 0.0));
1233    }
1234}