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