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