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.gen();
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 (simplified implementation)
460    fn compute_eigenvalues(hamiltonian: &Array2<Complex64>) -> QuantRS2Result<Vec<f64>> {
461        // This is a placeholder - in practice you'd use a proper eigenvalue solver
462        let dim = hamiltonian.nrows();
463        let mut eigenvalues: Vec<f64> = (0..dim).map(|i| hamiltonian[[i, i]].re).collect();
464        eigenvalues.sort_by(|a, b| {
465            a.partial_cmp(b)
466                .expect("Failed to compare eigenvalues in compute_eigenvalues")
467        });
468        Ok(eigenvalues)
469    }
470
471    /// Estimate energy gap using power iteration
472    fn estimate_gap(&self, _hamiltonian: &Array2<Complex64>) -> f64 {
473        // Simplified estimation - would use proper numerical methods in practice
474        let s = self.schedule.evaluate(self.current_time, self.total_time);
475        // Gap typically closes as sin(πs) near the end of annealing
476        (PI * s).sin() * 0.1 // Rough approximation
477    }
478
479    /// Check if adiabatic condition is satisfied
480    pub fn adiabatic_condition_satisfied(&self) -> QuantRS2Result<bool> {
481        let gap = self.energy_gap()?;
482        let s_dot = self.schedule.derivative(self.current_time, self.total_time);
483
484        // Adiabatic condition: |⟨1|dH/dt|0⟩| << Δ²
485        // Simplified check: gap should be much larger than evolution rate
486        Ok(gap > 10.0 * s_dot.abs())
487    }
488
489    /// Reset to initial state
490    pub fn reset(&mut self) {
491        let dim = 1 << self.num_qubits;
492        let amplitude = Complex64::new(1.0 / (dim as f64).sqrt(), 0.0);
493        self.state.fill(amplitude);
494        self.current_time = 0.0;
495    }
496
497    /// Get current state vector
498    pub const fn state(&self) -> &Array1<Complex64> {
499        &self.state
500    }
501
502    /// Get current annealing parameter s(t)
503    pub fn annealing_parameter(&self) -> f64 {
504        self.schedule.evaluate(self.current_time, self.total_time)
505    }
506
507    /// Get progress as percentage
508    pub fn progress(&self) -> f64 {
509        (self.current_time / self.total_time).min(1.0) * 100.0
510    }
511}
512
513/// Quantum annealing optimizer for solving optimization problems
514pub struct QuantumAnnealer {
515    computer: AdiabaticQuantumComputer,
516    problem: IsingProblem,
517    best_solution: Option<(Vec<i8>, f64)>,
518    history: Vec<QuantumAnnealingSnapshot>,
519}
520
521/// Snapshot of quantum annealing state at a point in time
522#[derive(Debug, Clone)]
523pub struct QuantumAnnealingSnapshot {
524    pub time: f64,
525    pub annealing_parameter: f64,
526    pub energy_gap: f64,
527    pub ground_state_probability: f64,
528    pub expected_energy: f64,
529}
530
531impl QuantumAnnealer {
532    /// Create a new quantum annealer
533    pub fn new(
534        problem: IsingProblem,
535        total_time: f64,
536        time_step: f64,
537        schedule: AnnealingSchedule,
538    ) -> Self {
539        let computer = AdiabaticQuantumComputer::new(&problem, total_time, time_step, schedule);
540
541        Self {
542            computer,
543            problem,
544            best_solution: None,
545            history: Vec::new(),
546        }
547    }
548
549    /// Run quantum annealing optimization
550    pub fn optimize(&mut self) -> QuantRS2Result<(Vec<i8>, f64)> {
551        self.computer.reset();
552        self.history.clear();
553
554        // Record initial state
555        self.record_snapshot()?;
556
557        // Run adiabatic evolution
558        while self.computer.current_time < self.computer.total_time {
559            self.computer.step()?;
560
561            // Record snapshots at regular intervals
562            if self.history.len() % 10 == 0 {
563                self.record_snapshot()?;
564            }
565        }
566
567        // Final measurement and solution extraction
568        let measurement = self.computer.measure();
569        let spins = self.state_to_spins(measurement);
570        let energy = self.problem.evaluate(&spins)?;
571
572        self.best_solution = Some((spins.clone(), energy));
573
574        Ok((spins, energy))
575    }
576
577    /// Run multiple annealing runs and return the best solution
578    pub fn optimize_multiple_runs(&mut self, num_runs: usize) -> QuantRS2Result<(Vec<i8>, f64)> {
579        let mut best_energy = f64::INFINITY;
580        let mut best_spins = vec![];
581
582        for _ in 0..num_runs {
583            let (spins, energy) = self.optimize()?;
584            if energy < best_energy {
585                best_energy = energy;
586                best_spins = spins;
587            }
588        }
589
590        Ok((best_spins, best_energy))
591    }
592
593    /// Record a snapshot of the current annealing state
594    fn record_snapshot(&mut self) -> QuantRS2Result<()> {
595        let time = self.computer.current_time;
596        let annealing_parameter = self.computer.annealing_parameter();
597        let energy_gap = self.computer.energy_gap()?;
598
599        // Calculate ground state probability and expected energy
600        let probs = self.computer.measurement_probabilities();
601        let ground_state_probability = probs[0]; // Assuming ground state is first
602
603        let mut expected_energy = 0.0;
604        for (state_idx, &prob) in probs.iter().enumerate() {
605            let spins = self.state_to_spins(state_idx);
606            let energy = self.problem.evaluate(&spins).unwrap_or(0.0);
607            expected_energy += prob * energy;
608        }
609
610        self.history.push(QuantumAnnealingSnapshot {
611            time,
612            annealing_parameter,
613            energy_gap,
614            ground_state_probability,
615            expected_energy,
616        });
617
618        Ok(())
619    }
620
621    /// Convert computational basis state to spin configuration
622    fn state_to_spins(&self, state: usize) -> Vec<i8> {
623        (0..self.problem.num_spins)
624            .map(|i| if (state >> i) & 1 == 0 { -1 } else { 1 })
625            .collect()
626    }
627
628    /// Get annealing history
629    pub fn history(&self) -> &[QuantumAnnealingSnapshot] {
630        &self.history
631    }
632
633    /// Get best solution found
634    pub const fn best_solution(&self) -> Option<&(Vec<i8>, f64)> {
635        self.best_solution.as_ref()
636    }
637
638    /// Get minimum energy gap during annealing
639    pub fn minimum_gap(&self) -> f64 {
640        self.history
641            .iter()
642            .map(|snapshot| snapshot.energy_gap)
643            .fold(f64::INFINITY, f64::min)
644    }
645
646    /// Calculate success probability (probability of finding ground state)
647    pub fn success_probability(&self) -> f64 {
648        self.history
649            .last()
650            .map_or(0.0, |snapshot| snapshot.ground_state_probability)
651    }
652}
653
654/// Create common optimization problems for testing
655pub struct ProblemGenerator;
656
657impl ProblemGenerator {
658    /// Generate random QUBO problem
659    pub fn random_qubo(num_vars: usize, density: f64) -> QUBOProblem {
660        let mut rng = thread_rng();
661
662        let mut q_matrix = Array2::zeros((num_vars, num_vars));
663
664        for i in 0..num_vars {
665            for j in i..num_vars {
666                if rng.gen::<f64>() < density {
667                    let value = rng.gen_range(-1.0..=1.0);
668                    q_matrix[[i, j]] = value;
669                    if i != j {
670                        q_matrix[[j, i]] = value; // Symmetric
671                    }
672                }
673            }
674        }
675
676        QUBOProblem::new(q_matrix)
677            .expect("Failed to create QUBO problem in random_qubo (matrix should be square)")
678    }
679
680    /// Generate MaxCut problem on a random graph
681    pub fn max_cut(num_vertices: usize, edge_probability: f64) -> IsingProblem {
682        let mut rng = thread_rng();
683
684        let h_fields = Array1::zeros(num_vertices);
685        let mut j_couplings = Array2::zeros((num_vertices, num_vertices));
686
687        // Generate random edges with positive coupling (ferromagnetic)
688        for i in 0..num_vertices {
689            for j in i + 1..num_vertices {
690                if rng.gen::<f64>() < edge_probability {
691                    let coupling = 1.0; // Unit weight edges
692                    j_couplings[[i, j]] = coupling;
693                    j_couplings[[j, i]] = coupling;
694                }
695            }
696        }
697
698        IsingProblem::new(h_fields, j_couplings)
699            .expect("Failed to create Ising problem in max_cut (matrix dimensions should match)")
700    }
701
702    /// Generate Number Partitioning problem
703    pub fn number_partitioning(numbers: Vec<f64>) -> IsingProblem {
704        let n = numbers.len();
705        let h_fields = Array1::zeros(n);
706        let mut j_couplings = Array2::zeros((n, n));
707
708        // J_ij = numbers[i] * numbers[j]
709        for i in 0..n {
710            for j in i + 1..n {
711                let coupling = numbers[i] * numbers[j];
712                j_couplings[[i, j]] = coupling;
713                j_couplings[[j, i]] = coupling;
714            }
715        }
716
717        IsingProblem::new(h_fields, j_couplings).expect("Failed to create Ising problem in number_partitioning (matrix dimensions should match)")
718    }
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724
725    #[test]
726    fn test_annealing_schedules() {
727        let linear = AnnealingSchedule::Linear;
728        assert_eq!(linear.evaluate(0.0, 10.0), 0.0);
729        assert_eq!(linear.evaluate(5.0, 10.0), 0.5);
730        assert_eq!(linear.evaluate(10.0, 10.0), 1.0);
731
732        let exp = AnnealingSchedule::Exponential { rate: 1.0 };
733        assert!(exp.evaluate(0.0, 1.0) < 0.1);
734        assert!(exp.evaluate(1.0, 1.0) > 0.6);
735
736        let poly = AnnealingSchedule::Polynomial { power: 2.0 };
737        assert_eq!(poly.evaluate(10.0, 10.0), 1.0);
738        assert!(poly.evaluate(5.0, 10.0) < linear.evaluate(5.0, 10.0));
739    }
740
741    #[test]
742    fn test_qubo_problem() {
743        let mut q_matrix = Array2::zeros((2, 2));
744        q_matrix[[0, 0]] = 1.0;
745        q_matrix[[1, 1]] = 1.0;
746        q_matrix[[0, 1]] = -2.0;
747        q_matrix[[1, 0]] = -2.0;
748
749        let qubo = QUBOProblem::new(q_matrix).expect("Failed to create QUBO in test_qubo_problem");
750
751        // Test evaluations
752        // For Q = [[1, -2], [-2, 1]]:
753        // [0,0]: 0*1*0 + 0*(-2)*0 + 0*(-2)*0 + 0*1*0 = 0
754        // [1,1]: 1*1*1 + 1*(-2)*1 + 1*(-2)*1 + 1*1*1 = 1 - 2 - 2 + 1 = -2
755        // [1,0]: 1*1*1 + 1*(-2)*0 + 0*(-2)*1 + 0*1*0 = 1
756        // [0,1]: 0*1*0 + 0*(-2)*1 + 1*(-2)*0 + 1*1*1 = 1
757        assert_eq!(
758            qubo.evaluate(&[0, 0])
759                .expect("Failed to evaluate [0,0] in test_qubo_problem"),
760            0.0
761        );
762        assert_eq!(
763            qubo.evaluate(&[1, 1])
764                .expect("Failed to evaluate [1,1] in test_qubo_problem"),
765            -2.0
766        );
767        assert_eq!(
768            qubo.evaluate(&[1, 0])
769                .expect("Failed to evaluate [1,0] in test_qubo_problem"),
770            1.0
771        );
772        assert_eq!(
773            qubo.evaluate(&[0, 1])
774                .expect("Failed to evaluate [0,1] in test_qubo_problem"),
775            1.0
776        );
777    }
778
779    #[test]
780    fn test_ising_problem() {
781        let h_fields = Array1::from(vec![0.5, -0.5]);
782        let mut j_couplings = Array2::zeros((2, 2));
783        j_couplings[[0, 1]] = 1.0;
784        j_couplings[[1, 0]] = 1.0;
785
786        let ising = IsingProblem::new(h_fields, j_couplings)
787            .expect("Failed to create Ising problem in test_ising_problem");
788
789        // Test evaluations
790        // Energy = -∑_i h_i s_i - ∑_{i<j} J_{ij} s_i s_j
791        // With h = [0.5, -0.5] and J_{01} = 1.0:
792        assert_eq!(
793            ising
794                .evaluate(&[-1, -1])
795                .expect("Failed to evaluate [-1,-1] in test_ising_problem"),
796            -1.0
797        ); // -(0.5*(-1) + (-0.5)*(-1)) - 1*(-1)*(-1) = -(-0.5 + 0.5) - 1 = -1
798        assert_eq!(
799            ising
800                .evaluate(&[1, 1])
801                .expect("Failed to evaluate [1,1] in test_ising_problem"),
802            -1.0
803        ); // -(0.5*1 + (-0.5)*1) - 1*1*1 = -(0.5 - 0.5) - 1 = -1
804        assert_eq!(
805            ising
806                .evaluate(&[1, -1])
807                .expect("Failed to evaluate [1,-1] in test_ising_problem"),
808            0.0
809        ); // -(0.5*1 + (-0.5)*(-1)) - 1*1*(-1) = -(0.5 + 0.5) - (-1) = -1 + 1 = 0
810        assert_eq!(
811            ising
812                .evaluate(&[-1, 1])
813                .expect("Failed to evaluate [-1,1] in test_ising_problem"),
814            2.0
815        ); // -(0.5*(-1) + (-0.5)*1) - 1*(-1)*1 = -(-0.5 - 0.5) - (-1) = 1 + 1 = 2
816    }
817
818    #[test]
819    fn test_qubo_to_ising_conversion() {
820        let mut q_matrix = Array2::zeros((2, 2));
821        q_matrix[[0, 1]] = 1.0;
822        q_matrix[[1, 0]] = 1.0;
823
824        let qubo = QUBOProblem::new(q_matrix)
825            .expect("Failed to create QUBO in test_qubo_to_ising_conversion");
826        let ising = qubo.to_ising();
827
828        // Verify the conversion maintains the same optimal solutions
829        // QUBO: minimize x₀x₁, optimal at (0,0), (0,1), (1,0) with value 0
830        // Ising: should have corresponding optimal spin configurations
831
832        assert_eq!(ising.num_spins, 2);
833        assert!(ising.h_fields.len() == 2);
834        assert!(ising.j_couplings.dim() == (2, 2));
835    }
836
837    #[test]
838    fn test_adiabatic_computer_initialization() {
839        let h_fields = Array1::from(vec![0.0, 0.0]);
840        let j_couplings = Array2::eye(2);
841        let ising = IsingProblem::new(h_fields, j_couplings)
842            .expect("Failed to create Ising problem in test_adiabatic_computer_initialization");
843
844        let adiabatic = AdiabaticQuantumComputer::new(
845            &ising,
846            10.0, // total_time
847            0.1,  // time_step
848            AnnealingSchedule::Linear,
849        );
850
851        assert_eq!(adiabatic.num_qubits, 2);
852        assert_eq!(adiabatic.current_time, 0.0);
853        assert_eq!(adiabatic.state.len(), 4); // 2^2 = 4 states
854
855        // Initial state should be uniform superposition
856        let expected_amplitude = 1.0 / 2.0; // 1/sqrt(4)
857        for amplitude in adiabatic.state.iter() {
858            assert!((amplitude.norm() - expected_amplitude).abs() < 1e-10);
859        }
860    }
861
862    #[test]
863    fn test_adiabatic_evolution_step() {
864        let h_fields = Array1::zeros(1);
865        let j_couplings = Array2::zeros((1, 1));
866        let ising = IsingProblem::new(h_fields, j_couplings)
867            .expect("Failed to create Ising problem in test_adiabatic_evolution_step");
868
869        let mut adiabatic = AdiabaticQuantumComputer::new(
870            &ising,
871            1.0, // total_time
872            0.1, // time_step
873            AnnealingSchedule::Linear,
874        );
875
876        let initial_time = adiabatic.current_time;
877        adiabatic
878            .step()
879            .expect("Failed to step adiabatic evolution in test_adiabatic_evolution_step");
880
881        assert!(adiabatic.current_time > initial_time);
882        assert!(adiabatic.current_time <= adiabatic.total_time);
883
884        // State should remain normalized
885        let norm = adiabatic.state.iter().map(|c| c.norm_sqr()).sum::<f64>();
886        assert!((norm - 1.0).abs() < 1e-10);
887    }
888
889    #[test]
890    fn test_quantum_annealer() {
891        let problem = ProblemGenerator::max_cut(3, 0.5);
892        let mut annealer = QuantumAnnealer::new(
893            problem,
894            5.0, // total_time
895            0.1, // time_step
896            AnnealingSchedule::Linear,
897        );
898
899        let (solution, energy) = annealer
900            .optimize()
901            .expect("Failed to optimize in test_quantum_annealer");
902
903        assert_eq!(solution.len(), 3);
904        assert!(solution.iter().all(|&s| s == -1 || s == 1));
905        assert!(energy.is_finite());
906
907        // Should have recorded history
908        assert!(!annealer.history().is_empty());
909
910        // Should have a best solution
911        assert!(annealer.best_solution().is_some());
912    }
913
914    #[test]
915    fn test_problem_generators() {
916        let qubo = ProblemGenerator::random_qubo(3, 0.5);
917        assert_eq!(qubo.num_vars, 3);
918        assert_eq!(qubo.q_matrix.dim(), (3, 3));
919
920        let max_cut = ProblemGenerator::max_cut(4, 0.6);
921        assert_eq!(max_cut.num_spins, 4);
922        assert_eq!(max_cut.h_fields.len(), 4);
923        assert_eq!(max_cut.j_couplings.dim(), (4, 4));
924
925        let numbers = vec![1.0, 2.0, 3.0];
926        let num_part = ProblemGenerator::number_partitioning(numbers);
927        assert_eq!(num_part.num_spins, 3);
928    }
929
930    #[test]
931    fn test_multiple_annealing_runs() {
932        let problem = ProblemGenerator::random_qubo(2, 1.0).to_ising();
933        let mut annealer = QuantumAnnealer::new(
934            problem,
935            2.0, // total_time
936            0.2, // time_step
937            AnnealingSchedule::Linear,
938        );
939
940        let (solution, energy) = annealer
941            .optimize_multiple_runs(3)
942            .expect("Failed to optimize multiple runs in test_multiple_annealing_runs");
943
944        assert_eq!(solution.len(), 2);
945        assert!(solution.iter().all(|&s| s == -1 || s == 1));
946        assert!(energy.is_finite());
947    }
948
949    #[test]
950    fn test_annealing_parameter_progression() {
951        let problem = ProblemGenerator::max_cut(2, 1.0);
952        let mut computer = AdiabaticQuantumComputer::new(
953            &problem,
954            1.0,  // total_time
955            0.25, // time_step (4 steps total)
956            AnnealingSchedule::Linear,
957        );
958
959        // Check annealing parameter at different times
960        assert_eq!(computer.annealing_parameter(), 0.0);
961
962        computer
963            .step()
964            .expect("Failed to step (1) in test_annealing_parameter_progression");
965        assert!((computer.annealing_parameter() - 0.25).abs() < 1e-10);
966
967        computer
968            .step()
969            .expect("Failed to step (2) in test_annealing_parameter_progression");
970        assert!((computer.annealing_parameter() - 0.5).abs() < 1e-10);
971
972        computer
973            .step()
974            .expect("Failed to step (3) in test_annealing_parameter_progression");
975        assert!((computer.annealing_parameter() - 0.75).abs() < 1e-10);
976
977        computer
978            .step()
979            .expect("Failed to step (4) in test_annealing_parameter_progression");
980        assert!((computer.annealing_parameter() - 1.0).abs() < 1e-10);
981    }
982
983    #[test]
984    fn test_energy_gap_calculation() {
985        let h_fields = Array1::from(vec![1.0]);
986        let j_couplings = Array2::zeros((1, 1));
987        let ising = IsingProblem::new(h_fields, j_couplings)
988            .expect("Failed to create Ising problem in test_energy_gap_calculation");
989
990        let computer = AdiabaticQuantumComputer::new(&ising, 1.0, 0.1, AnnealingSchedule::Linear);
991
992        let gap = computer
993            .energy_gap()
994            .expect("Failed to calculate energy gap in test_energy_gap_calculation");
995        assert!(gap >= 0.0);
996    }
997
998    #[test]
999    fn test_measurement_probabilities() {
1000        let h_fields = Array1::zeros(1);
1001        let j_couplings = Array2::zeros((1, 1));
1002        let ising = IsingProblem::new(h_fields, j_couplings)
1003            .expect("Failed to create Ising problem in test_measurement_probabilities");
1004
1005        let computer = AdiabaticQuantumComputer::new(&ising, 1.0, 0.1, AnnealingSchedule::Linear);
1006
1007        let probs = computer.measurement_probabilities();
1008        assert_eq!(probs.len(), 2); // 2^1 = 2 states
1009
1010        // Probabilities should sum to 1
1011        let total: f64 = probs.iter().sum();
1012        assert!((total - 1.0).abs() < 1e-10);
1013
1014        // All probabilities should be non-negative
1015        assert!(probs.iter().all(|&p| p >= 0.0));
1016    }
1017}