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