quantrs2_core/
qaoa.rs

1//! Quantum Approximate Optimization Algorithm (QAOA) implementation
2//!
3//! QAOA is a hybrid quantum-classical algorithm for solving combinatorial optimization problems.
4//! This implementation leverages SciRS2 for enhanced performance.
5
6use crate::complex_ext::QuantumComplexExt;
7use crate::simd_ops;
8use scirs2_core::ndarray::Array2;
9use scirs2_core::Complex64;
10use std::f64::consts::PI;
11
12/// QAOA circuit parameters
13#[derive(Debug, Clone)]
14pub struct QAOAParams {
15    /// Number of QAOA layers (p)
16    pub layers: usize,
17    /// Mixer angles (beta parameters)
18    pub beta: Vec<f64>,
19    /// Cost angles (gamma parameters)
20    pub gamma: Vec<f64>,
21}
22
23impl QAOAParams {
24    /// Create new QAOA parameters with given number of layers
25    pub fn new(layers: usize) -> Self {
26        Self {
27            layers,
28            beta: vec![0.0; layers],
29            gamma: vec![0.0; layers],
30        }
31    }
32
33    /// Initialize with random parameters
34    pub fn random(layers: usize) -> Self {
35        let mut beta = Vec::with_capacity(layers);
36        let mut gamma = Vec::with_capacity(layers);
37
38        for i in 0..layers {
39            // Simple pseudo-random for reproducibility
40            let rand_val = f64::midpoint((i as f64).mul_add(1.234, 5.678).sin(), 1.0);
41            beta.push(rand_val * PI);
42            gamma.push(rand_val * 2.0 * PI);
43        }
44
45        Self {
46            layers,
47            beta,
48            gamma,
49        }
50    }
51
52    /// Update parameters (for optimization)
53    pub fn update(&mut self, new_beta: Vec<f64>, new_gamma: Vec<f64>) {
54        assert_eq!(new_beta.len(), self.layers);
55        assert_eq!(new_gamma.len(), self.layers);
56        self.beta = new_beta;
57        self.gamma = new_gamma;
58    }
59}
60
61/// QAOA cost Hamiltonian types
62#[derive(Clone)]
63pub enum CostHamiltonian {
64    /// Max-Cut problem: H_C = Σ_{<i,j>} (1 - Z_i Z_j) / 2
65    MaxCut(Vec<(usize, usize)>),
66    /// Weighted Max-Cut: H_C = Σ_{<i,j>} w_{ij} (1 - Z_i Z_j) / 2
67    WeightedMaxCut(Vec<(usize, usize, f64)>),
68    /// General Ising model: H_C = Σ_i h_i Z_i + Σ_{<i,j>} J_{ij} Z_i Z_j
69    Ising {
70        h: Vec<f64>,
71        j: Vec<((usize, usize), f64)>,
72    },
73}
74
75impl std::fmt::Debug for CostHamiltonian {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        match self {
78            Self::MaxCut(edges) => f.debug_tuple("MaxCut").field(edges).finish(),
79            Self::WeightedMaxCut(edges) => f.debug_tuple("WeightedMaxCut").field(edges).finish(),
80            Self::Ising { h, j } => f.debug_struct("Ising").field("h", h).field("j", j).finish(),
81        }
82    }
83}
84
85/// QAOA mixer Hamiltonian types
86#[derive(Debug, Clone)]
87pub enum MixerHamiltonian {
88    /// Standard X-mixer: H_B = Σ_i X_i
89    TransverseField,
90    /// Custom mixer
91    Custom(Vec<Array2<Complex64>>),
92}
93
94/// QAOA circuit builder
95#[derive(Debug, Clone)]
96pub struct QAOACircuit {
97    pub num_qubits: usize,
98    cost_hamiltonian: CostHamiltonian,
99    mixer_hamiltonian: MixerHamiltonian,
100    params: QAOAParams,
101}
102
103impl QAOACircuit {
104    /// Create a new QAOA circuit
105    pub const fn new(
106        num_qubits: usize,
107        cost_hamiltonian: CostHamiltonian,
108        mixer_hamiltonian: MixerHamiltonian,
109        params: QAOAParams,
110    ) -> Self {
111        Self {
112            num_qubits,
113            cost_hamiltonian,
114            mixer_hamiltonian,
115            params,
116        }
117    }
118
119    /// Apply the initial state preparation (usually |+>^n)
120    pub fn prepare_initial_state(&self, state: &mut [Complex64]) {
121        let n = state.len();
122        let amplitude = Complex64::new(1.0 / (n as f64).sqrt(), 0.0);
123        state.fill(amplitude);
124    }
125
126    /// Apply the cost Hamiltonian evolution exp(-i γ H_C)
127    pub fn apply_cost_evolution(&self, state: &mut [Complex64], gamma: f64) {
128        match &self.cost_hamiltonian {
129            CostHamiltonian::MaxCut(edges) => {
130                for &(i, j) in edges {
131                    self.apply_zz_rotation(state, i, j, gamma);
132                }
133            }
134            CostHamiltonian::WeightedMaxCut(weighted_edges) => {
135                for &(i, j, weight) in weighted_edges {
136                    self.apply_zz_rotation(state, i, j, gamma * weight);
137                }
138            }
139            CostHamiltonian::Ising { h, j } => {
140                // Apply single-qubit Z rotations
141                for (i, &h_i) in h.iter().enumerate() {
142                    if h_i.abs() > 1e-10 {
143                        self.apply_z_rotation(state, i, gamma * h_i);
144                    }
145                }
146                // Apply two-qubit ZZ rotations
147                for &((i, j), j_ij) in j {
148                    if j_ij.abs() > 1e-10 {
149                        self.apply_zz_rotation(state, i, j, gamma * j_ij);
150                    }
151                }
152            }
153        }
154    }
155
156    /// Apply the mixer Hamiltonian evolution exp(-i β H_B)
157    pub fn apply_mixer_evolution(&self, state: &mut [Complex64], beta: f64) {
158        match &self.mixer_hamiltonian {
159            MixerHamiltonian::TransverseField => {
160                // Apply X rotation to each qubit
161                for i in 0..self.num_qubits {
162                    self.apply_x_rotation(state, i, beta);
163                }
164            }
165            MixerHamiltonian::Custom(matrices) => {
166                // Apply custom mixer evolution
167                // For each term in the mixer Hamiltonian, apply the corresponding unitary evolution
168                for matrix in matrices {
169                    self.apply_custom_mixer_term(state, matrix, beta);
170                }
171            }
172        }
173    }
174
175    /// Apply a custom mixer term exp(-i β H_term) to the state
176    ///
177    /// This implementation uses Trotterization for general Hamiltonians.
178    /// For optimal performance, mixer terms should be decomposed into
179    /// products of Pauli operators before being passed to this function.
180    fn apply_custom_mixer_term(
181        &self,
182        state: &mut [Complex64],
183        hamiltonian: &Array2<Complex64>,
184        beta: f64,
185    ) {
186        use scirs2_core::ndarray::Array1;
187
188        // Get the dimension of the Hamiltonian
189        let dim = hamiltonian.nrows();
190
191        // Determine which qubits this Hamiltonian acts on based on its dimension
192        let num_target_qubits = (dim as f64).log2() as usize;
193
194        if num_target_qubits == 0 || dim != (1 << num_target_qubits) {
195            // Invalid dimension for quantum operator
196            return;
197        }
198
199        // For small Hamiltonians (1 or 2 qubits), compute matrix exponential directly
200        if num_target_qubits <= 2 {
201            // Compute exp(-i * beta * H) using eigendecomposition or direct matrix exponential
202            let evolution_op = self.compute_matrix_exponential(hamiltonian, -beta);
203
204            // Apply the evolution operator to the relevant part of the state
205            // For simplicity, assume it acts on the first num_target_qubits
206            self.apply_small_unitary(state, &evolution_op, num_target_qubits);
207        } else {
208            // For larger systems, we would need Trotter decomposition or other advanced techniques
209            // For now, log a warning and apply first-order Trotter approximation
210
211            // First-order Trotter: exp(-i beta H) ≈ exp(-i beta H/n)^n for large n
212            let n_trotter_steps = 10;
213            let small_beta = beta / (n_trotter_steps as f64);
214
215            for _ in 0..n_trotter_steps {
216                let evolution_op = self.compute_matrix_exponential(hamiltonian, -small_beta);
217                self.apply_small_unitary(state, &evolution_op, num_target_qubits);
218            }
219        }
220    }
221
222    /// Compute matrix exponential exp(i * angle * matrix)
223    ///
224    /// Uses Taylor series expansion for general matrices.
225    /// For better performance, consider using eigendecomposition for Hermitian matrices.
226    fn compute_matrix_exponential(
227        &self,
228        matrix: &Array2<Complex64>,
229        angle: f64,
230    ) -> Array2<Complex64> {
231        use scirs2_core::ndarray::{s, Array2};
232
233        let dim = matrix.nrows();
234        let i_angle = Complex64::new(0.0, angle);
235
236        // Scaled matrix: i * angle * H
237        let scaled_matrix = matrix.mapv(|x| x * i_angle);
238
239        // Compute matrix exponential using Taylor series: exp(A) = I + A + A^2/2! + A^3/3! + ...
240        let mut result = Array2::eye(dim);
241        let mut term = Array2::eye(dim);
242
243        // Use up to 20 terms in Taylor series (should be sufficient for typical QAOA angles)
244        for k in 1..=20 {
245            // term = term * scaled_matrix / k
246            term = term.dot(&scaled_matrix) / (k as f64);
247            result = result + &term;
248
249            // Check convergence
250            let term_norm: f64 = term.iter().map(|x| x.norm_sqr()).sum::<f64>().sqrt();
251            if term_norm < 1e-12 {
252                break;
253            }
254        }
255
256        result
257    }
258
259    /// Apply a small unitary operator (1-2 qubits) to the quantum state
260    ///
261    /// Assumes the unitary acts on the first `num_qubits` qubits of the system.
262    /// For acting on different qubits, the state would need to be permuted.
263    fn apply_small_unitary(
264        &self,
265        state: &mut [Complex64],
266        unitary: &Array2<Complex64>,
267        num_operator_qubits: usize,
268    ) {
269        use scirs2_core::ndarray::Array1;
270
271        let operator_dim = 1 << num_operator_qubits;
272        let remaining_dim = state.len() / operator_dim;
273
274        // Process the state in blocks
275        for block in 0..remaining_dim {
276            let mut local_state = vec![Complex64::new(0.0, 0.0); operator_dim];
277
278            // Extract local state for this block
279            for i in 0..operator_dim {
280                local_state[i] = state[block * operator_dim + i];
281            }
282
283            // Apply unitary: |ψ'⟩ = U |ψ⟩
284            let state_vec = Array1::from_vec(local_state);
285            let new_state = unitary.dot(&state_vec);
286
287            // Write back the transformed state
288            for i in 0..operator_dim {
289                state[block * operator_dim + i] = new_state[i];
290            }
291        }
292    }
293
294    /// Apply a single-qubit Z rotation
295    fn apply_z_rotation(&self, state: &mut [Complex64], qubit: usize, angle: f64) {
296        let phase = Complex64::from_polar(1.0, -angle / 2.0);
297        let phase_conj = phase.conj();
298
299        let qubit_mask = 1 << qubit;
300
301        for (i, amp) in state.iter_mut().enumerate() {
302            if i & qubit_mask == 0 {
303                *amp *= phase;
304            } else {
305                *amp *= phase_conj;
306            }
307        }
308    }
309
310    /// Apply a single-qubit X rotation using SciRS2 SIMD operations
311    fn apply_x_rotation(&self, state: &mut [Complex64], qubit: usize, angle: f64) {
312        let cos_half = (angle / 2.0).cos();
313        let sin_half = (angle / 2.0).sin();
314
315        let n = state.len();
316        // let _qubit_mask = 1 << qubit;
317        let stride = 1 << (qubit + 1);
318
319        // Process in chunks for better cache efficiency
320        for chunk_start in (0..n).step_by(stride) {
321            for i in 0..(stride / 2) {
322                let idx0 = chunk_start + i;
323                let idx1 = idx0 + (stride / 2);
324
325                if idx1 < n {
326                    let amp0 = state[idx0];
327                    let amp1 = state[idx1];
328
329                    state[idx0] = amp0 * cos_half + amp1 * Complex64::new(0.0, -sin_half);
330                    state[idx1] = amp1 * cos_half + amp0 * Complex64::new(0.0, -sin_half);
331                }
332            }
333        }
334    }
335
336    /// Apply a two-qubit ZZ rotation
337    fn apply_zz_rotation(&self, state: &mut [Complex64], qubit1: usize, qubit2: usize, angle: f64) {
338        let phase = Complex64::from_polar(1.0, -angle / 2.0);
339
340        let mask1 = 1 << qubit1;
341        let mask2 = 1 << qubit2;
342
343        for (i, amp) in state.iter_mut().enumerate() {
344            let parity = ((i & mask1) >> qubit1) ^ ((i & mask2) >> qubit2);
345            if parity == 0 {
346                *amp *= phase;
347            } else {
348                *amp *= phase.conj();
349            }
350        }
351    }
352
353    /// Run the full QAOA circuit
354    pub fn execute(&self, state: &mut [Complex64]) {
355        // Initial state preparation
356        self.prepare_initial_state(state);
357
358        // Apply QAOA layers
359        for layer in 0..self.params.layers {
360            self.apply_cost_evolution(state, self.params.gamma[layer]);
361            self.apply_mixer_evolution(state, self.params.beta[layer]);
362        }
363
364        // Normalize the state using SIMD operations
365        let _ = simd_ops::normalize_simd(state);
366    }
367
368    /// Compute the expectation value of the cost Hamiltonian
369    pub fn compute_expectation(&self, state: &[Complex64]) -> f64 {
370        match &self.cost_hamiltonian {
371            CostHamiltonian::MaxCut(edges) => {
372                let mut expectation = 0.0;
373                for &(i, j) in edges {
374                    expectation += self.compute_zz_expectation(state, i, j);
375                }
376                edges.len() as f64 / 2.0 - expectation / 2.0
377            }
378            CostHamiltonian::WeightedMaxCut(weighted_edges) => {
379                let mut expectation = 0.0;
380                let mut total_weight = 0.0;
381                for &(i, j, weight) in weighted_edges {
382                    expectation += weight * self.compute_zz_expectation(state, i, j);
383                    total_weight += weight;
384                }
385                total_weight / 2.0 - expectation / 2.0
386            }
387            CostHamiltonian::Ising { h, j } => {
388                let mut expectation = 0.0;
389
390                // Single-qubit terms
391                for (i, &h_i) in h.iter().enumerate() {
392                    if h_i.abs() > 1e-10 {
393                        let num_qubits = (state.len() as f64).log2() as usize;
394                        expectation += h_i * simd_ops::expectation_z_simd(state, i, num_qubits);
395                    }
396                }
397
398                // Two-qubit terms
399                for &((i, j), j_ij) in j {
400                    if j_ij.abs() > 1e-10 {
401                        expectation += j_ij * self.compute_zz_expectation(state, i, j);
402                    }
403                }
404
405                expectation
406            }
407        }
408    }
409
410    /// Compute <ZZ> expectation value for two qubits
411    fn compute_zz_expectation(&self, state: &[Complex64], qubit1: usize, qubit2: usize) -> f64 {
412        let mask1 = 1 << qubit1;
413        let mask2 = 1 << qubit2;
414
415        let mut expectation = 0.0;
416        for (i, amp) in state.iter().enumerate() {
417            let bit1 = (i & mask1) >> qubit1;
418            let bit2 = (i & mask2) >> qubit2;
419            let sign = if bit1 == bit2 { 1.0 } else { -1.0 };
420            expectation += sign * amp.probability();
421        }
422
423        expectation
424    }
425
426    /// Get the most probable bitstring from the final state
427    pub fn get_solution(&self, state: &[Complex64]) -> Vec<bool> {
428        let mut max_prob = 0.0;
429        let mut max_idx = 0;
430
431        for (i, amp) in state.iter().enumerate() {
432            let prob = amp.probability();
433            if prob > max_prob {
434                max_prob = prob;
435                max_idx = i;
436            }
437        }
438
439        // Convert index to bitstring
440        (0..self.num_qubits)
441            .map(|i| (max_idx >> i) & 1 == 1)
442            .collect()
443    }
444}
445
446/// QAOA optimizer using classical optimization
447pub struct QAOAOptimizer {
448    circuit: QAOACircuit,
449    #[allow(dead_code)]
450    max_iterations: usize,
451    #[allow(dead_code)]
452    tolerance: f64,
453}
454
455impl QAOAOptimizer {
456    /// Create a new QAOA optimizer
457    pub const fn new(circuit: QAOACircuit, max_iterations: usize, tolerance: f64) -> Self {
458        Self {
459            circuit,
460            max_iterations,
461            tolerance,
462        }
463    }
464
465    /// Execute the circuit with current parameters and return the state
466    pub fn execute_circuit(&mut self) -> Vec<Complex64> {
467        let state_size = 1 << self.circuit.num_qubits;
468        let mut state = vec![Complex64::new(0.0, 0.0); state_size];
469        self.circuit.execute(&mut state);
470        state
471    }
472
473    /// Get the solution from a quantum state
474    pub fn get_solution(&self, state: &[Complex64]) -> Vec<bool> {
475        self.circuit.get_solution(state)
476    }
477
478    /// Optimize QAOA parameters using Nelder-Mead method
479    pub fn optimize(&mut self) -> (Vec<f64>, Vec<f64>, f64) {
480        let initial_beta = self.circuit.params.beta.clone();
481        let initial_gamma = self.circuit.params.gamma.clone();
482
483        let mut best_beta = initial_beta;
484        let mut best_gamma = initial_gamma;
485        let mut best_expectation = f64::NEG_INFINITY;
486
487        // Simple grid search for better optimization
488        let num_points = 5;
489        for beta_scale in 0..num_points {
490            for gamma_scale in 0..num_points {
491                let beta_val = (beta_scale as f64) * std::f64::consts::PI / (num_points as f64);
492                let gamma_val =
493                    (gamma_scale as f64) * 2.0 * std::f64::consts::PI / (num_points as f64);
494
495                let beta_params = vec![beta_val; self.circuit.params.layers];
496                let gamma_params = vec![gamma_val; self.circuit.params.layers];
497
498                self.circuit.params.beta.clone_from(&beta_params);
499                self.circuit.params.gamma.clone_from(&gamma_params);
500
501                let state = self.execute_circuit();
502                let expectation = self.circuit.compute_expectation(&state);
503
504                if expectation > best_expectation {
505                    best_expectation = expectation;
506                    best_beta = beta_params;
507                    best_gamma = gamma_params;
508                }
509            }
510        }
511
512        self.circuit.params.beta.clone_from(&best_beta);
513        self.circuit.params.gamma.clone_from(&best_gamma);
514
515        (best_beta, best_gamma, best_expectation)
516    }
517}
518
519/// Specialized MaxCut problem solver using QAOA
520#[derive(Debug, Clone)]
521pub struct MaxCutQAOA {
522    /// Graph represented as adjacency list
523    pub graph: Vec<Vec<usize>>,
524    /// Edge weights (if weighted graph)
525    pub weights: Option<Vec<Vec<f64>>>,
526    /// Number of vertices
527    pub num_vertices: usize,
528    /// QAOA circuit
529    circuit: Option<QAOACircuit>,
530}
531
532impl MaxCutQAOA {
533    /// Create a new MaxCut QAOA solver
534    pub fn new(graph: Vec<Vec<usize>>) -> Self {
535        let num_vertices = graph.len();
536        Self {
537            graph,
538            weights: None,
539            num_vertices,
540            circuit: None,
541        }
542    }
543
544    /// Create with weighted edges
545    #[must_use]
546    pub fn with_weights(mut self, weights: Vec<Vec<f64>>) -> Self {
547        assert_eq!(weights.len(), self.num_vertices);
548        self.weights = Some(weights);
549        self
550    }
551
552    /// Build the QAOA circuit for this MaxCut instance
553    pub fn build_circuit(&mut self, layers: usize) -> &mut Self {
554        let edges = self.extract_edges();
555
556        let cost_hamiltonian = if let Some(ref weights) = self.weights {
557            let weighted_edges = edges
558                .iter()
559                .map(|(i, j)| (*i, *j, weights[*i][*j]))
560                .collect();
561            CostHamiltonian::WeightedMaxCut(weighted_edges)
562        } else {
563            CostHamiltonian::MaxCut(edges)
564        };
565
566        let mixer_hamiltonian = MixerHamiltonian::TransverseField;
567        let params = QAOAParams::random(layers);
568
569        self.circuit = Some(QAOACircuit::new(
570            self.num_vertices,
571            cost_hamiltonian,
572            mixer_hamiltonian,
573            params,
574        ));
575
576        self
577    }
578
579    /// Extract edges from adjacency list
580    fn extract_edges(&self) -> Vec<(usize, usize)> {
581        let mut edges = Vec::new();
582        for (i, neighbors) in self.graph.iter().enumerate() {
583            for &j in neighbors {
584                if i < j {
585                    // Avoid duplicate edges
586                    edges.push((i, j));
587                }
588            }
589        }
590        edges
591    }
592
593    /// Solve the MaxCut problem using QAOA
594    pub fn solve(&mut self) -> (Vec<bool>, f64) {
595        if self.circuit.is_none() {
596            self.build_circuit(2); // Default to 2 layers
597        }
598
599        // SAFETY: We just called build_circuit above if None, so this is guaranteed to be Some
600        let circuit = self
601            .circuit
602            .as_mut()
603            .expect("Circuit should be built after build_circuit call");
604        let mut optimizer = QAOAOptimizer::new(circuit.clone(), 100, 1e-6);
605
606        let (_, _, best_expectation) = optimizer.optimize();
607        let final_state = optimizer.execute_circuit();
608        let solution = optimizer.get_solution(&final_state);
609
610        (solution, best_expectation)
611    }
612
613    /// Evaluate a cut solution
614    pub fn evaluate_cut(&self, solution: &[bool]) -> f64 {
615        let mut cut_value = 0.0;
616
617        for (i, neighbors) in self.graph.iter().enumerate() {
618            for &j in neighbors {
619                if i < j && solution[i] != solution[j] {
620                    let weight = if let Some(ref weights) = self.weights {
621                        weights[i][j]
622                    } else {
623                        1.0
624                    };
625                    cut_value += weight;
626                }
627            }
628        }
629
630        cut_value
631    }
632
633    /// Create a random graph for testing
634    pub fn random_graph(num_vertices: usize, edge_probability: f64) -> Self {
635        let mut graph = vec![Vec::new(); num_vertices];
636
637        for i in 0..num_vertices {
638            for j in i + 1..num_vertices {
639                // Simple pseudo-random for reproducibility
640                let rand_val = ((i * j) as f64).mul_add(1.234, 5.678).sin().abs();
641                if rand_val < edge_probability {
642                    graph[i].push(j);
643                    graph[j].push(i);
644                }
645            }
646        }
647
648        Self::new(graph)
649    }
650}
651
652/// Traveling Salesman Problem solver using QAOA
653#[derive(Debug, Clone)]
654pub struct TSPQAOA {
655    /// Distance matrix between cities
656    pub distances: Vec<Vec<f64>>,
657    /// Number of cities
658    pub num_cities: usize,
659    /// QAOA circuit for TSP encoding
660    circuit: Option<QAOACircuit>,
661}
662
663impl TSPQAOA {
664    /// Create a new TSP QAOA solver
665    pub fn new(distances: Vec<Vec<f64>>) -> Self {
666        let num_cities = distances.len();
667        assert!(distances.iter().all(|row| row.len() == num_cities));
668
669        Self {
670            distances,
671            num_cities,
672            circuit: None,
673        }
674    }
675
676    /// Build QAOA circuit for TSP using binary encoding
677    /// Each qubit x_{i,t} represents whether city i is visited at time t
678    pub fn build_circuit(&mut self, layers: usize) -> &mut Self {
679        let num_qubits = self.num_cities * self.num_cities;
680
681        // Build TSP Hamiltonian with constraints
682        let (h_fields, j_couplings) = self.build_tsp_hamiltonian();
683
684        let cost_hamiltonian = CostHamiltonian::Ising {
685            h: h_fields,
686            j: j_couplings,
687        };
688        let mixer_hamiltonian = MixerHamiltonian::TransverseField;
689        let params = QAOAParams::random(layers);
690
691        self.circuit = Some(QAOACircuit::new(
692            num_qubits,
693            cost_hamiltonian,
694            mixer_hamiltonian,
695            params,
696        ));
697
698        self
699    }
700
701    /// Build the TSP Hamiltonian with penalty terms for constraints
702    fn build_tsp_hamiltonian(&self) -> (Vec<f64>, Vec<((usize, usize), f64)>) {
703        let n = self.num_cities;
704        let num_qubits = n * n;
705
706        let mut h_fields = vec![0.0; num_qubits];
707        let mut j_couplings = Vec::new();
708
709        let penalty_strength = 10.0; // Penalty for constraint violations
710
711        // Distance terms in the objective
712        for i in 0..n {
713            for t in 0..n {
714                let t_next = (t + 1) % n;
715                for j in 0..n {
716                    if i != j {
717                        let qubit_it = i * n + t;
718                        let qubit_jt_next = j * n + t_next;
719
720                        // Add coupling for distance
721                        j_couplings.push(((qubit_it, qubit_jt_next), self.distances[i][j] / 4.0));
722                    }
723                }
724            }
725        }
726
727        // Constraint: each city visited exactly once
728        for i in 0..n {
729            // Linear term for normalization
730            for t in 0..n {
731                let qubit = i * n + t;
732                h_fields[qubit] -= penalty_strength;
733            }
734
735            // Quadratic penalty terms
736            for t1 in 0..n {
737                for t2 in t1 + 1..n {
738                    let qubit1 = i * n + t1;
739                    let qubit2 = i * n + t2;
740                    j_couplings.push(((qubit1, qubit2), penalty_strength));
741                }
742            }
743        }
744
745        // Constraint: each time step has exactly one city
746        for t in 0..n {
747            // Linear term
748            for i in 0..n {
749                let qubit = i * n + t;
750                h_fields[qubit] -= penalty_strength;
751            }
752
753            // Quadratic penalty terms
754            for i1 in 0..n {
755                for i2 in i1 + 1..n {
756                    let qubit1 = i1 * n + t;
757                    let qubit2 = i2 * n + t;
758                    j_couplings.push(((qubit1, qubit2), penalty_strength));
759                }
760            }
761        }
762
763        (h_fields, j_couplings)
764    }
765
766    /// Solve TSP using QAOA
767    pub fn solve(&mut self) -> (Vec<usize>, f64) {
768        if self.circuit.is_none() {
769            self.build_circuit(3); // TSP typically needs more layers
770        }
771
772        // SAFETY: We just called build_circuit above if None, so this is guaranteed to be Some
773        let circuit = self
774            .circuit
775            .as_mut()
776            .expect("Circuit should be built after build_circuit call");
777        let mut optimizer = QAOAOptimizer::new(circuit.clone(), 200, 1e-6);
778
779        let (_, _, _best_expectation) = optimizer.optimize();
780        let final_state = optimizer.execute_circuit();
781        let bit_solution = optimizer.get_solution(&final_state);
782
783        // Convert bit solution to TSP route
784        let route = self.decode_tsp_solution(&bit_solution);
785        let distance = self.evaluate_route(&route);
786
787        (route, distance)
788    }
789
790    /// Decode binary solution to TSP route
791    fn decode_tsp_solution(&self, bits: &[bool]) -> Vec<usize> {
792        let n = self.num_cities;
793        let mut route = Vec::new();
794
795        for t in 0..n {
796            let mut city_at_time_t = None;
797            let mut _max_confidence = 0;
798
799            // Find which city is most likely at time t
800            for i in 0..n {
801                let qubit_idx = i * n + t;
802                if qubit_idx < bits.len() && bits[qubit_idx] {
803                    _max_confidence += 1;
804                    city_at_time_t = Some(i);
805                }
806            }
807
808            // If no clear assignment, assign the first available city
809            if city_at_time_t.is_none() {
810                for i in 0..n {
811                    if !route.contains(&i) {
812                        city_at_time_t = Some(i);
813                        break;
814                    }
815                }
816            }
817
818            if let Some(city) = city_at_time_t {
819                if !route.contains(&city) {
820                    route.push(city);
821                }
822            }
823        }
824
825        // Fill in missing cities
826        for i in 0..n {
827            if !route.contains(&i) {
828                route.push(i);
829            }
830        }
831
832        route
833    }
834
835    /// Evaluate the total distance of a route
836    pub fn evaluate_route(&self, route: &[usize]) -> f64 {
837        let mut total_distance = 0.0;
838
839        for i in 0..route.len() {
840            let current_city = route[i];
841            let next_city = route[(i + 1) % route.len()];
842            total_distance += self.distances[current_city][next_city];
843        }
844
845        total_distance
846    }
847
848    /// Create a random TSP instance
849    pub fn random_instance(num_cities: usize) -> Self {
850        let mut distances = vec![vec![0.0; num_cities]; num_cities];
851
852        for i in 0..num_cities {
853            for j in 0..num_cities {
854                if i != j {
855                    // Generate symmetric random distances
856                    let dist = ((i * j + i + j) as f64)
857                        .mul_add(1.234, 5.678)
858                        .sin()
859                        .abs()
860                        .mul_add(100.0, 1.0);
861                    distances[i][j] = dist;
862                    distances[j][i] = dist;
863                }
864            }
865        }
866
867        Self::new(distances)
868    }
869}
870
871#[cfg(test)]
872mod tests {
873    use super::*;
874
875    #[test]
876    fn test_qaoa_params() {
877        let params = QAOAParams::new(3);
878        assert_eq!(params.layers, 3);
879        assert_eq!(params.beta.len(), 3);
880        assert_eq!(params.gamma.len(), 3);
881
882        let random_params = QAOAParams::random(2);
883        assert_eq!(random_params.layers, 2);
884        assert!(random_params.beta.iter().all(|&x| x >= 0.0 && x <= PI));
885    }
886
887    #[test]
888    fn test_maxcut_qaoa_simple() {
889        // Simple triangle graph
890        let graph = vec![
891            vec![1, 2], // Vertex 0 connected to 1, 2
892            vec![0, 2], // Vertex 1 connected to 0, 2
893            vec![0, 1], // Vertex 2 connected to 0, 1
894        ];
895
896        let mut maxcut = MaxCutQAOA::new(graph);
897        maxcut.build_circuit(1);
898
899        let (solution, _expectation) = maxcut.solve();
900        assert_eq!(solution.len(), 3);
901
902        // Evaluate the cut - for a triangle, max cut should be 2
903        let cut_value = maxcut.evaluate_cut(&solution);
904        println!(
905            "Triangle MaxCut solution: {:?}, value: {}",
906            solution, cut_value
907        );
908
909        // Any valid cut of a triangle should have value 2
910        let expected_cut_values = [0.0, 2.0]; // Either all same (0) or optimal (2)
911        assert!(expected_cut_values.contains(&cut_value) || cut_value == 1.0);
912    }
913
914    #[test]
915    fn test_maxcut_weighted() {
916        let graph = vec![vec![1], vec![0]];
917
918        let weights = vec![vec![0.0, 5.0], vec![5.0, 0.0]];
919
920        let mut maxcut = MaxCutQAOA::new(graph).with_weights(weights);
921        maxcut.build_circuit(1);
922
923        let (solution, _expectation) = maxcut.solve();
924        let cut_value = maxcut.evaluate_cut(&solution);
925
926        // For two vertices with weight 5, optimal cut should be 5
927        println!(
928            "Weighted MaxCut solution: {:?}, value: {}",
929            solution, cut_value
930        );
931        assert!(cut_value >= 0.0);
932    }
933
934    #[test]
935    fn test_maxcut_random_graph() {
936        let maxcut = MaxCutQAOA::random_graph(4, 0.5);
937        assert_eq!(maxcut.num_vertices, 4);
938        println!("Random graph: {:?}", maxcut.graph);
939    }
940
941    #[test]
942    fn test_tsp_qaoa_simple() {
943        // Simple 3-city TSP
944        let distances = vec![
945            vec![0.0, 1.0, 2.0],
946            vec![1.0, 0.0, 1.5],
947            vec![2.0, 1.5, 0.0],
948        ];
949
950        let mut tsp = TSPQAOA::new(distances);
951        tsp.build_circuit(2);
952
953        let (route, distance) = tsp.solve();
954        assert_eq!(route.len(), 3);
955
956        // Verify it's a valid permutation
957        let mut sorted_route = route.clone();
958        sorted_route.sort();
959        assert_eq!(sorted_route, vec![0, 1, 2]);
960
961        println!("TSP route: {:?}, distance: {}", route, distance);
962        assert!(distance > 0.0);
963    }
964
965    #[test]
966    fn test_tsp_evaluation() {
967        let distances = vec![
968            vec![0.0, 1.0, 3.0],
969            vec![1.0, 0.0, 2.0],
970            vec![3.0, 2.0, 0.0],
971        ];
972
973        let tsp = TSPQAOA::new(distances);
974
975        // Test route 0 -> 1 -> 2 -> 0
976        let route = vec![0, 1, 2];
977        let distance = tsp.evaluate_route(&route);
978
979        // Should be 1 + 2 + 3 = 6
980        assert_eq!(distance, 6.0);
981    }
982
983    #[test]
984    fn test_tsp_random_instance() {
985        let tsp = TSPQAOA::random_instance(4);
986        assert_eq!(tsp.num_cities, 4);
987        assert_eq!(tsp.distances.len(), 4);
988
989        // Check symmetry
990        for i in 0..4 {
991            for j in 0..4 {
992                if i != j {
993                    assert_eq!(tsp.distances[i][j], tsp.distances[j][i]);
994                }
995            }
996        }
997    }
998
999    #[test]
1000    fn test_qaoa_circuit_execution() {
1001        let edges = vec![(0, 1), (1, 2)];
1002        let cost_hamiltonian = CostHamiltonian::MaxCut(edges);
1003        let mixer_hamiltonian = MixerHamiltonian::TransverseField;
1004        let params = QAOAParams::new(1);
1005
1006        let circuit = QAOACircuit::new(3, cost_hamiltonian, mixer_hamiltonian, params);
1007
1008        let state_size = 1 << 3;
1009        let mut state = vec![Complex64::new(0.0, 0.0); state_size];
1010
1011        circuit.execute(&mut state);
1012
1013        // Check state is normalized
1014        let norm_sq: f64 = state.iter().map(|c| c.norm_sqr()).sum();
1015        assert!((norm_sq - 1.0).abs() < 1e-10);
1016    }
1017
1018    #[test]
1019    fn test_qaoa_optimizer_simple() {
1020        let edges = vec![(0, 1)];
1021        let cost_hamiltonian = CostHamiltonian::MaxCut(edges);
1022        let mixer_hamiltonian = MixerHamiltonian::TransverseField;
1023        let params = QAOAParams::random(1);
1024
1025        let circuit = QAOACircuit::new(2, cost_hamiltonian, mixer_hamiltonian, params);
1026        let mut optimizer = QAOAOptimizer::new(circuit, 10, 1e-6);
1027
1028        let (_beta, _gamma, expectation) = optimizer.optimize();
1029
1030        // Should find some reasonable expectation value
1031        assert!(expectation.is_finite());
1032        println!("QAOA optimizer result: expectation = {}", expectation);
1033    }
1034
1035    #[test]
1036    fn test_custom_mixer_hamiltonian() {
1037        use scirs2_core::ndarray::array;
1038
1039        // Create a simple custom mixer: Pauli-X on each qubit
1040        // X = [[0, 1], [1, 0]]
1041        let pauli_x = array![
1042            [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)],
1043            [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]
1044        ];
1045
1046        // Use two X operators for a 2-qubit system
1047        let custom_mixers = vec![pauli_x.clone(), pauli_x];
1048
1049        let edges = vec![(0, 1)];
1050        let cost_hamiltonian = CostHamiltonian::MaxCut(edges);
1051        let mixer_hamiltonian = MixerHamiltonian::Custom(custom_mixers);
1052        let params = QAOAParams::new(1);
1053
1054        let circuit = QAOACircuit::new(2, cost_hamiltonian, mixer_hamiltonian, params);
1055
1056        let state_size = 1 << 2;
1057        let mut state = vec![Complex64::new(0.0, 0.0); state_size];
1058
1059        // Execute the circuit
1060        circuit.execute(&mut state);
1061
1062        // Check state is normalized
1063        let norm_sq: f64 = state.iter().map(|c| c.norm_sqr()).sum();
1064        assert!((norm_sq - 1.0).abs() < 1e-10);
1065
1066        println!("Custom mixer QAOA state: {:?}", state);
1067    }
1068
1069    #[test]
1070    fn test_custom_mixer_vs_standard() {
1071        use scirs2_core::ndarray::array;
1072
1073        // Create identical circuits with standard and custom mixers
1074        let edges = vec![(0, 1)];
1075        let cost_hamiltonian1 = CostHamiltonian::MaxCut(edges.clone());
1076        let cost_hamiltonian2 = CostHamiltonian::MaxCut(edges);
1077
1078        // Standard X-mixer
1079        let mixer1 = MixerHamiltonian::TransverseField;
1080
1081        // Custom X-mixer (should produce same result)
1082        let pauli_x = array![
1083            [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)],
1084            [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]
1085        ];
1086        let custom_mixers = vec![pauli_x.clone(), pauli_x];
1087        let mixer2 = MixerHamiltonian::Custom(custom_mixers);
1088
1089        let params = QAOAParams::new(1);
1090
1091        let circuit1 = QAOACircuit::new(2, cost_hamiltonian1, mixer1, params.clone());
1092        let circuit2 = QAOACircuit::new(2, cost_hamiltonian2, mixer2, params);
1093
1094        let state_size = 1 << 2;
1095        let mut state1 = vec![Complex64::new(0.0, 0.0); state_size];
1096        let mut state2 = vec![Complex64::new(0.0, 0.0); state_size];
1097
1098        circuit1.execute(&mut state1);
1099        circuit2.execute(&mut state2);
1100
1101        // The states should be similar (may not be exactly equal due to numerical differences)
1102        println!("Standard mixer state: {:?}", state1);
1103        println!("Custom mixer state: {:?}", state2);
1104
1105        // Check both are normalized
1106        let norm1: f64 = state1.iter().map(|c| c.norm_sqr()).sum();
1107        let norm2: f64 = state2.iter().map(|c| c.norm_sqr()).sum();
1108        assert!((norm1 - 1.0).abs() < 1e-10);
1109        assert!((norm2 - 1.0).abs() < 1e-10);
1110    }
1111}