Skip to main content

quantrs2_ml/
variational.rs

1//! Variational Quantum Algorithms: VQE, QAOA, and hybrid solvers.
2//!
3//! [`VariationalSolver`] executes parameterised ansatz circuits on a
4//! state-vector backend and minimises an expectation-value objective using
5//! classical optimisers, supporting both VQE (chemistry) and QAOA (combinatorics).
6
7use crate::error::{MLError, Result};
8use crate::optimization::Optimizer;
9use quantrs2_circuit::prelude::Circuit;
10use scirs2_core::ndarray::Array1;
11use scirs2_core::random::prelude::*;
12use scirs2_core::Complex64;
13
14/// Algorithm type for variational quantum algorithms
15#[derive(Debug, Clone, Copy)]
16pub enum VariationalAlgorithm {
17    /// Variational Quantum Eigensolver
18    VQE,
19
20    /// Quantum Approximate Optimization Algorithm
21    QAOA,
22
23    /// Quantum Support Vector Machine
24    QSVM,
25
26    /// Quantum Neural Network
27    QNN,
28
29    /// Custom variational algorithm
30    Custom,
31}
32
33/// Ansatz type for variational circuits
34#[derive(Debug, Clone, Copy)]
35pub enum AnsatzType {
36    /// Hardware efficient ansatz
37    HardwareEfficient,
38
39    /// Unitary Coupled Cluster Singles and Doubles
40    UCCSD,
41
42    /// QAOA ansatz
43    QAOA,
44
45    /// Custom ansatz
46    Custom,
47}
48
49/// Variational quantum circuit with parameterized gates
50#[derive(Debug, Clone)]
51pub struct VariationalCircuit {
52    /// Number of qubits
53    pub num_qubits: usize,
54
55    /// Number of parameters
56    pub num_params: usize,
57
58    /// Current parameters
59    pub parameters: Array1<f64>,
60
61    /// Number of layers
62    pub num_layers: usize,
63
64    /// Type of ansatz
65    pub ansatz_type: AnsatzType,
66}
67
68impl VariationalCircuit {
69    /// Creates a new variational circuit
70    pub fn new(
71        num_qubits: usize,
72        num_params: usize,
73        num_layers: usize,
74        ansatz_type: AnsatzType,
75    ) -> Result<Self> {
76        // Initialize random parameters
77        let parameters = Array1::from_vec(
78            (0..num_params)
79                .map(|_| thread_rng().random::<f64>() * 2.0 * std::f64::consts::PI)
80                .collect(),
81        );
82
83        Ok(VariationalCircuit {
84            num_qubits,
85            num_params,
86            parameters,
87            num_layers,
88            ansatz_type,
89        })
90    }
91
92    /// Creates a circuit with the current parameters
93    pub fn create_circuit<const N: usize>(&self) -> Result<Circuit<N>> {
94        // This is a dummy implementation
95        // In a real system, this would create a circuit based on the ansatz type and parameters
96
97        let mut circuit = Circuit::<N>::new();
98
99        for i in 0..N.min(self.num_qubits) {
100            // Apply some dummy gates based on parameters
101            circuit.h(i)?;
102
103            if i < self.parameters.len() {
104                circuit.rz(i, self.parameters[i])?;
105            }
106        }
107
108        // Add entanglement based on the ansatz type
109        match self.ansatz_type {
110            AnsatzType::HardwareEfficient => {
111                // Linear nearest-neighbor entanglement
112                for i in 0..N.min(self.num_qubits) - 1 {
113                    circuit.cnot(i, i + 1)?;
114                }
115            }
116            AnsatzType::UCCSD => {
117                // More complex entanglement pattern
118                for i in 0..N.min(self.num_qubits) / 2 {
119                    let j = N.min(self.num_qubits) / 2 + i;
120                    if j < N {
121                        circuit.cnot(i, j)?;
122                    }
123                }
124            }
125            AnsatzType::QAOA => {
126                // QAOA-style entanglement (fully connected)
127                for i in 0..N.min(self.num_qubits) {
128                    for j in i + 1..N.min(self.num_qubits) {
129                        circuit.cnot(i, j)?;
130                    }
131                }
132            }
133            AnsatzType::Custom => {
134                // Custom entanglement pattern
135                if N >= 3 {
136                    circuit.cnot(0, 1)?;
137                    circuit.cnot(1, 2)?;
138                    if N > 3 {
139                        circuit.cnot(2, 3)?;
140                    }
141                }
142            }
143        }
144
145        Ok(circuit)
146    }
147
148    /// Computes the expectation value of a Hamiltonian via direct statevector simulation.
149    ///
150    /// `hamiltonian` is a slice of `(coefficient, pauli_term_list)` tuples.
151    /// Each `pauli_term_list` contains `(qubit_index, pauli_type)` pairs where
152    /// `pauli_type` encodes 0=I, 1=X, 2=Y, 3=Z.
153    ///
154    /// The ansatz consists of `num_layers` repetitions of:
155    ///   – H gate on every qubit
156    ///   – RZ(θ_i) on qubit i using the i-th parameter (cycling if parameters run out)
157    ///   – CNOT chain (linear nearest-neighbour entanglement)
158    ///
159    /// Returns `Σ_k  coef_k · ⟨ψ | P_k | ψ⟩`.
160    pub fn compute_expectation(&self, hamiltonian: &[(f64, Vec<(usize, usize)>)]) -> Result<f64> {
161        let n = self.num_qubits;
162        if n == 0 {
163            return Ok(0.0);
164        }
165        let dim = 1usize << n;
166
167        // Build the statevector |ψ⟩ by applying the ansatz to |0⟩^n.
168        let mut state = vec![Complex64::new(0.0, 0.0); dim];
169        state[0] = Complex64::new(1.0, 0.0);
170
171        let mut param_idx = 0usize;
172
173        for _layer in 0..self.num_layers.max(1) {
174            // --- Hadamard on every qubit ---
175            for q in 0..n {
176                apply_single_qubit_h(&mut state, q, n);
177            }
178
179            // --- RZ(θ) on every qubit ---
180            for q in 0..n {
181                let theta = if self.parameters.is_empty() {
182                    0.0
183                } else {
184                    self.parameters[param_idx % self.parameters.len()]
185                };
186                param_idx += 1;
187                apply_single_qubit_rz(&mut state, q, n, theta);
188            }
189
190            // --- CNOT chain (linear nearest-neighbour) ---
191            for q in 0..n.saturating_sub(1) {
192                apply_cnot(&mut state, q, q + 1, n);
193            }
194        }
195
196        // --- Evaluate ⟨ψ | H | ψ⟩ ---
197        let mut expectation = 0.0f64;
198
199        for (coef, pauli_terms) in hamiltonian {
200            // Compute ⟨ψ | P | ψ⟩ where P = ⊗ Pauli operators.
201            // Action of P on basis state |i⟩: produces coeff * |j⟩.
202            // ⟨ψ|P|ψ⟩ = Σ_i  ψ*_i · (P|ψ⟩)_i
203            //          = Σ_i  ψ*_i · Σ_j  ⟨i|P|j⟩ ψ_j
204            let mut psi_p = vec![Complex64::new(0.0, 0.0); dim];
205            for j in 0..dim {
206                if state[j].norm_sqr() < 1e-30 {
207                    continue;
208                }
209                let mut coeff = state[j];
210                let mut target = j;
211                for &(qubit, pauli_type) in pauli_terms {
212                    if qubit >= n {
213                        continue;
214                    }
215                    let bit = (j >> qubit) & 1;
216                    match pauli_type {
217                        0 => {} // I – identity
218                        1 => {
219                            // X flips qubit
220                            target ^= 1 << qubit;
221                        }
222                        2 => {
223                            // Y flips qubit, phase +i if bit=0, -i if bit=1
224                            target ^= 1 << qubit;
225                            coeff *= if bit == 0 {
226                                Complex64::new(0.0, 1.0)
227                            } else {
228                                Complex64::new(0.0, -1.0)
229                            };
230                        }
231                        3 => {
232                            // Z: phase -1 if bit=1
233                            if bit == 1 {
234                                coeff *= Complex64::new(-1.0, 0.0);
235                            }
236                        }
237                        _ => {} // unknown – treat as identity
238                    }
239                }
240                if target < dim {
241                    psi_p[target] += coeff;
242                }
243            }
244            // ⟨ψ|P|ψ⟩ = Σ_i  ψ*_i · (P|ψ⟩)_i
245            let pauli_exp: Complex64 = state
246                .iter()
247                .zip(psi_p.iter())
248                .map(|(a, b)| a.conj() * b)
249                .sum();
250            expectation += coef * pauli_exp.re;
251        }
252
253        Ok(expectation)
254    }
255
256    /// Evaluates the objective function for optimization
257    pub fn evaluate(&self, objective: &dyn Fn(&VariationalCircuit) -> Result<f64>) -> Result<f64> {
258        objective(self)
259    }
260
261    /// Optimizes the circuit parameters using the parameter-shift rule for gradient computation.
262    ///
263    /// For each parameter θ_i the gradient is estimated as:
264    ///   ∂f/∂θ_i ≈ (f(θ_i + π/2) − f(θ_i − π/2)) / 2
265    ///
266    /// A gradient-descent update is then applied with the learning rate from the
267    /// supplied [`Optimizer`].  Other optimizer variants fall back to `lr = 0.01`.
268    pub fn optimize(
269        &mut self,
270        objective: &dyn Fn(&VariationalCircuit) -> Result<f64>,
271        optimizer: &Optimizer,
272        max_iterations: usize,
273    ) -> Result<f64> {
274        let shift = std::f64::consts::PI / 2.0;
275        let lr = match optimizer {
276            Optimizer::GradientDescent { learning_rate } => *learning_rate,
277            Optimizer::Adam { learning_rate, .. } => *learning_rate,
278            Optimizer::SPSA { learning_rate, .. } => *learning_rate,
279            Optimizer::QuantumNaturalGradient { learning_rate, .. } => *learning_rate,
280            Optimizer::SciRS2 { config, .. } => {
281                config.get("learning_rate").copied().unwrap_or(0.01)
282            }
283        };
284
285        let mut best_value = self.evaluate(objective)?;
286
287        for _ in 0..max_iterations {
288            let n_params = self.parameters.len();
289            let mut gradient = vec![0.0f64; n_params];
290
291            for i in 0..n_params {
292                let mut plus = self.clone();
293                plus.parameters[i] += shift;
294                let mut minus = self.clone();
295                minus.parameters[i] -= shift;
296                gradient[i] = (plus.evaluate(objective)? - minus.evaluate(objective)?) * 0.5;
297            }
298
299            for i in 0..n_params {
300                self.parameters[i] -= lr * gradient[i];
301            }
302
303            let new_value = self.evaluate(objective)?;
304            if new_value < best_value {
305                best_value = new_value;
306            }
307        }
308
309        Ok(best_value)
310    }
311}
312
313// ---------------------------------------------------------------------------
314// Statevector helper functions (runtime-dimension, no const generic N)
315// ---------------------------------------------------------------------------
316
317/// Apply a single-qubit Hadamard gate to qubit `q` in a `n`-qubit statevector.
318fn apply_single_qubit_h(state: &mut Vec<Complex64>, q: usize, n: usize) {
319    let inv_sqrt2 = 1.0 / std::f64::consts::SQRT_2;
320    let dim = 1 << n;
321    let half_step = 1 << q;
322    let step = half_step << 1;
323    let mut base = 0;
324    while base < dim {
325        for i in base..base + half_step {
326            let a = state[i];
327            let b = state[i + half_step];
328            state[i] = Complex64::new((a.re + b.re) * inv_sqrt2, (a.im + b.im) * inv_sqrt2);
329            state[i + half_step] =
330                Complex64::new((a.re - b.re) * inv_sqrt2, (a.im - b.im) * inv_sqrt2);
331        }
332        base += step;
333    }
334}
335
336/// Apply RZ(θ) on qubit `q`: |0⟩ → e^{-iθ/2}|0⟩, |1⟩ → e^{+iθ/2}|1⟩.
337fn apply_single_qubit_rz(state: &mut Vec<Complex64>, q: usize, n: usize, theta: f64) {
338    let dim = 1 << n;
339    let phase0 = Complex64::from_polar(1.0, -theta * 0.5);
340    let phase1 = Complex64::from_polar(1.0, theta * 0.5);
341    for i in 0..dim {
342        let bit = (i >> q) & 1;
343        state[i] *= if bit == 0 { phase0 } else { phase1 };
344    }
345}
346
347/// Apply CNOT with `control` and `target` qubits.
348fn apply_cnot(state: &mut Vec<Complex64>, control: usize, target: usize, n: usize) {
349    let dim = 1 << n;
350    for i in 0..dim {
351        if (i >> control) & 1 == 1 {
352            let j = i ^ (1 << target);
353            if i < j {
354                state.swap(i, j);
355            }
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_optimize_reduces_objective() {
366        // Simple convex objective: minimize Σ θ_i²
367        // Parameter-shift gradient: ∂(θ_i²)/∂θ_i = 2θ_i
368        // But parameter-shift gives (f(θ+π/2) - f(θ-π/2))/2 = (sin(θ+π/2)^2 - sin(θ-π/2)^2)/2
369        // For the pure quadratic we just need descent, not exact gradients.
370        let num_qubits = 2;
371        let num_params = 4;
372        let num_layers = 1;
373        let mut vc = VariationalCircuit::new(
374            num_qubits,
375            num_params,
376            num_layers,
377            AnsatzType::HardwareEfficient,
378        )
379        .expect("circuit creation failed");
380        // Override parameters to known starting values so the test is deterministic.
381        vc.parameters = Array1::from_vec(vec![1.0, 1.0, 1.0, 1.0]);
382
383        let objective = |vc: &VariationalCircuit| -> Result<f64> {
384            Ok(vc.parameters.iter().map(|p| p * p).sum())
385        };
386
387        let initial = objective(&vc).expect("initial evaluation failed");
388        let optimizer = Optimizer::GradientDescent { learning_rate: 0.1 };
389        let result = vc
390            .optimize(&objective, &optimizer, 20)
391            .expect("optimization failed");
392
393        assert!(
394            result < initial,
395            "optimize should reduce objective: {result} vs initial {initial}"
396        );
397    }
398
399    #[test]
400    fn test_compute_expectation_identity_hamiltonian() {
401        // H = 1.0 * I (identity on qubit 0) — ⟨ψ|I|ψ⟩ = 1 always
402        let vc = VariationalCircuit::new(2, 2, 1, AnsatzType::HardwareEfficient)
403            .expect("circuit creation failed");
404        let hamiltonian = vec![(1.0, vec![(0_usize, 0_usize)])]; // coef=1, I on qubit 0
405        let exp = vc
406            .compute_expectation(&hamiltonian)
407            .expect("expectation failed");
408        assert!((exp - 1.0).abs() < 1e-10, "⟨I⟩ should be 1.0, got {exp}");
409    }
410}