quantrs2_sim/
autodiff_vqe.rs

1//! Automatic differentiation for Variational Quantum Eigensolver (VQE).
2//!
3//! This module implements automatic differentiation techniques specifically designed
4//! for variational quantum algorithms, including parameter-shift rule, finite differences,
5//! and optimization strategies for VQE.
6
7use ndarray::{Array1, Array2};
8use num_complex::Complex64;
9use std::f64::consts::PI;
10
11use crate::error::{Result, SimulatorError};
12use crate::pauli::{PauliOperatorSum, PauliString};
13use crate::statevector::StateVectorSimulator;
14use quantrs2_core::gate::GateOp;
15
16/// Gradient computation method
17#[derive(Debug, Clone, Copy)]
18pub enum GradientMethod {
19    /// Parameter-shift rule (exact for quantum gates)
20    ParameterShift,
21    /// Finite differences
22    FiniteDifference { step_size: f64 },
23    /// Simultaneous perturbation stochastic approximation
24    SPSA { step_size: f64 },
25}
26
27/// Automatic differentiation context for tracking gradients
28#[derive(Debug, Clone)]
29pub struct AutoDiffContext {
30    /// Parameter values
31    pub parameters: Vec<f64>,
32    /// Parameter names/indices
33    pub parameter_names: Vec<String>,
34    /// Gradient computation method
35    pub method: GradientMethod,
36    /// Current gradients
37    pub gradients: Vec<f64>,
38    /// Gradient computation count
39    pub grad_evaluations: usize,
40    /// Function evaluation count
41    pub func_evaluations: usize,
42}
43
44impl AutoDiffContext {
45    /// Create new autodiff context
46    pub fn new(parameters: Vec<f64>, method: GradientMethod) -> Self {
47        let num_params = parameters.len();
48        Self {
49            parameters,
50            parameter_names: (0..num_params).map(|i| format!("θ{}", i)).collect(),
51            method,
52            gradients: vec![0.0; num_params],
53            grad_evaluations: 0,
54            func_evaluations: 0,
55        }
56    }
57
58    /// Set parameter names
59    pub fn with_parameter_names(mut self, names: Vec<String>) -> Self {
60        assert_eq!(names.len(), self.parameters.len());
61        self.parameter_names = names;
62        self
63    }
64
65    /// Update parameters
66    pub fn update_parameters(&mut self, new_params: Vec<f64>) {
67        assert_eq!(new_params.len(), self.parameters.len());
68        self.parameters = new_params;
69    }
70
71    /// Get parameter by name
72    pub fn get_parameter(&self, name: &str) -> Option<f64> {
73        self.parameter_names
74            .iter()
75            .position(|n| n == name)
76            .map(|i| self.parameters[i])
77    }
78
79    /// Set parameter by name
80    pub fn set_parameter(&mut self, name: &str, value: f64) -> Result<()> {
81        if let Some(i) = self.parameter_names.iter().position(|n| n == name) {
82            self.parameters[i] = value;
83            Ok(())
84        } else {
85            Err(SimulatorError::InvalidInput(format!(
86                "Parameter '{}' not found",
87                name
88            )))
89        }
90    }
91}
92
93/// Parametric quantum gate that supports automatic differentiation
94pub trait ParametricGate: Send + Sync {
95    /// Get gate name
96    fn name(&self) -> &str;
97
98    /// Get qubits this gate acts on
99    fn qubits(&self) -> Vec<usize>;
100
101    /// Get parameter indices this gate depends on
102    fn parameter_indices(&self) -> Vec<usize>;
103
104    /// Evaluate gate matrix given parameter values
105    fn matrix(&self, params: &[f64]) -> Result<Array2<Complex64>>;
106
107    /// Compute gradient of gate matrix with respect to each parameter
108    fn gradient(&self, params: &[f64], param_idx: usize) -> Result<Array2<Complex64>>;
109
110    /// Apply parameter-shift rule for this gate
111    fn parameter_shift_gradient(
112        &self,
113        params: &[f64],
114        param_idx: usize,
115    ) -> Result<(Array2<Complex64>, Array2<Complex64>)> {
116        let shift = PI / 2.0;
117        let mut params_plus = params.to_vec();
118        let mut params_minus = params.to_vec();
119
120        if param_idx < params.len() {
121            params_plus[param_idx] += shift;
122            params_minus[param_idx] -= shift;
123        }
124
125        let matrix_plus = self.matrix(&params_plus)?;
126        let matrix_minus = self.matrix(&params_minus)?;
127
128        Ok((matrix_plus, matrix_minus))
129    }
130}
131
132/// Parametric rotation gates
133#[derive(Debug, Clone)]
134pub struct ParametricRX {
135    pub qubit: usize,
136    pub param_idx: usize,
137}
138
139impl ParametricGate for ParametricRX {
140    fn name(&self) -> &str {
141        "RX"
142    }
143
144    fn qubits(&self) -> Vec<usize> {
145        vec![self.qubit]
146    }
147
148    fn parameter_indices(&self) -> Vec<usize> {
149        vec![self.param_idx]
150    }
151
152    fn matrix(&self, params: &[f64]) -> Result<Array2<Complex64>> {
153        let theta = params[self.param_idx];
154        let cos_half = (theta / 2.0).cos();
155        let sin_half = (theta / 2.0).sin();
156
157        Ok(ndarray::array![
158            [Complex64::new(cos_half, 0.), Complex64::new(0., -sin_half)],
159            [Complex64::new(0., -sin_half), Complex64::new(cos_half, 0.)]
160        ])
161    }
162
163    fn gradient(&self, params: &[f64], param_idx: usize) -> Result<Array2<Complex64>> {
164        if param_idx != self.param_idx {
165            return Ok(Array2::zeros((2, 2)));
166        }
167
168        let theta = params[self.param_idx];
169        let cos_half = (theta / 2.0).cos();
170        let sin_half = (theta / 2.0).sin();
171
172        // d/dθ RX(θ) = -i/2 * X * RX(θ)
173        Ok(ndarray::array![
174            [
175                Complex64::new(-sin_half / 2.0, 0.),
176                Complex64::new(0., -cos_half / 2.0)
177            ],
178            [
179                Complex64::new(0., -cos_half / 2.0),
180                Complex64::new(-sin_half / 2.0, 0.)
181            ]
182        ])
183    }
184}
185
186#[derive(Debug, Clone)]
187pub struct ParametricRY {
188    pub qubit: usize,
189    pub param_idx: usize,
190}
191
192impl ParametricGate for ParametricRY {
193    fn name(&self) -> &str {
194        "RY"
195    }
196
197    fn qubits(&self) -> Vec<usize> {
198        vec![self.qubit]
199    }
200
201    fn parameter_indices(&self) -> Vec<usize> {
202        vec![self.param_idx]
203    }
204
205    fn matrix(&self, params: &[f64]) -> Result<Array2<Complex64>> {
206        let theta = params[self.param_idx];
207        let cos_half = (theta / 2.0).cos();
208        let sin_half = (theta / 2.0).sin();
209
210        Ok(ndarray::array![
211            [Complex64::new(cos_half, 0.), Complex64::new(-sin_half, 0.)],
212            [Complex64::new(sin_half, 0.), Complex64::new(cos_half, 0.)]
213        ])
214    }
215
216    fn gradient(&self, params: &[f64], param_idx: usize) -> Result<Array2<Complex64>> {
217        if param_idx != self.param_idx {
218            return Ok(Array2::zeros((2, 2)));
219        }
220
221        let theta = params[self.param_idx];
222        let cos_half = (theta / 2.0).cos();
223        let sin_half = (theta / 2.0).sin();
224
225        Ok(ndarray::array![
226            [
227                Complex64::new(-sin_half / 2.0, 0.),
228                Complex64::new(-cos_half / 2.0, 0.)
229            ],
230            [
231                Complex64::new(cos_half / 2.0, 0.),
232                Complex64::new(-sin_half / 2.0, 0.)
233            ]
234        ])
235    }
236}
237
238#[derive(Debug, Clone)]
239pub struct ParametricRZ {
240    pub qubit: usize,
241    pub param_idx: usize,
242}
243
244impl ParametricGate for ParametricRZ {
245    fn name(&self) -> &str {
246        "RZ"
247    }
248
249    fn qubits(&self) -> Vec<usize> {
250        vec![self.qubit]
251    }
252
253    fn parameter_indices(&self) -> Vec<usize> {
254        vec![self.param_idx]
255    }
256
257    fn matrix(&self, params: &[f64]) -> Result<Array2<Complex64>> {
258        let theta = params[self.param_idx];
259        let exp_pos = Complex64::from_polar(1.0, theta / 2.0);
260        let exp_neg = Complex64::from_polar(1.0, -theta / 2.0);
261
262        Ok(ndarray::array![
263            [exp_neg, Complex64::new(0., 0.)],
264            [Complex64::new(0., 0.), exp_pos]
265        ])
266    }
267
268    fn gradient(&self, params: &[f64], param_idx: usize) -> Result<Array2<Complex64>> {
269        if param_idx != self.param_idx {
270            return Ok(Array2::zeros((2, 2)));
271        }
272
273        let theta = params[self.param_idx];
274        let exp_pos = Complex64::from_polar(1.0, theta / 2.0);
275        let exp_neg = Complex64::from_polar(1.0, -theta / 2.0);
276
277        Ok(ndarray::array![
278            [exp_neg * Complex64::new(0., -0.5), Complex64::new(0., 0.)],
279            [Complex64::new(0., 0.), exp_pos * Complex64::new(0., 0.5)]
280        ])
281    }
282}
283
284/// Parametric quantum circuit for VQE
285pub struct ParametricCircuit {
286    /// Sequence of parametric gates
287    pub gates: Vec<Box<dyn ParametricGate>>,
288    /// Number of qubits
289    pub num_qubits: usize,
290    /// Number of parameters
291    pub num_parameters: usize,
292}
293
294impl ParametricCircuit {
295    /// Create new parametric circuit
296    pub fn new(num_qubits: usize) -> Self {
297        Self {
298            gates: Vec::new(),
299            num_qubits,
300            num_parameters: 0,
301        }
302    }
303
304    /// Add a parametric gate
305    pub fn add_gate(&mut self, gate: Box<dyn ParametricGate>) {
306        // Update parameter count
307        for &param_idx in &gate.parameter_indices() {
308            self.num_parameters = self.num_parameters.max(param_idx + 1);
309        }
310        self.gates.push(gate);
311    }
312
313    /// Add RX gate
314    pub fn rx(&mut self, qubit: usize, param_idx: usize) {
315        self.add_gate(Box::new(ParametricRX { qubit, param_idx }));
316    }
317
318    /// Add RY gate
319    pub fn ry(&mut self, qubit: usize, param_idx: usize) {
320        self.add_gate(Box::new(ParametricRY { qubit, param_idx }));
321    }
322
323    /// Add RZ gate
324    pub fn rz(&mut self, qubit: usize, param_idx: usize) {
325        self.add_gate(Box::new(ParametricRZ { qubit, param_idx }));
326    }
327
328    /// Evaluate circuit for given parameters and return final state
329    pub fn evaluate(&self, params: &[f64]) -> Result<Array1<Complex64>> {
330        if params.len() != self.num_parameters {
331            return Err(SimulatorError::InvalidInput(format!(
332                "Expected {} parameters, got {}",
333                self.num_parameters,
334                params.len()
335            )));
336        }
337
338        // Initialize state vector simulator
339        let mut simulator = StateVectorSimulator::new();
340
341        // Apply gates sequentially
342        for gate in &self.gates {
343            let matrix = gate.matrix(params)?;
344            let qubits = gate.qubits();
345
346            if qubits.len() == 1 {
347                // Single-qubit gate - would need proper simulator integration
348                // For now, this is a placeholder
349            } else if qubits.len() == 2 {
350                // Two-qubit gate - would need proper simulator integration
351                // For now, this is a placeholder
352            }
353        }
354
355        // Return placeholder state for now
356        let mut state = Array1::zeros(1 << self.num_qubits);
357        state[0] = Complex64::new(1.0, 0.0); // |0...0>
358        Ok(state)
359    }
360
361    /// Compute gradient of expectation value using parameter-shift rule
362    pub fn gradient_expectation(
363        &self,
364        observable: &PauliOperatorSum,
365        params: &[f64],
366        method: GradientMethod,
367    ) -> Result<Vec<f64>> {
368        match method {
369            GradientMethod::ParameterShift => self.parameter_shift_gradient(observable, params),
370            GradientMethod::FiniteDifference { step_size } => {
371                self.finite_difference_gradient(observable, params, step_size)
372            }
373            GradientMethod::SPSA { step_size } => self.spsa_gradient(observable, params, step_size),
374        }
375    }
376
377    /// Parameter-shift rule gradient computation
378    fn parameter_shift_gradient(
379        &self,
380        observable: &PauliOperatorSum,
381        params: &[f64],
382    ) -> Result<Vec<f64>> {
383        let mut gradients = vec![0.0; self.num_parameters];
384
385        // Use parameter-shift rule: ∂⟨H⟩/∂θᵢ = (⟨H⟩₊ - ⟨H⟩₋) / 2
386        // where ±π/2 shifts are applied to parameter θᵢ
387
388        for param_idx in 0..self.num_parameters {
389            let shift = PI / 2.0;
390
391            // Forward shift
392            let mut params_plus = params.to_vec();
393            params_plus[param_idx] += shift;
394            let state_plus = self.evaluate(&params_plus)?;
395            let expectation_plus = compute_expectation_value(&state_plus, observable)?;
396
397            // Backward shift
398            let mut params_minus = params.to_vec();
399            params_minus[param_idx] -= shift;
400            let state_minus = self.evaluate(&params_minus)?;
401            let expectation_minus = compute_expectation_value(&state_minus, observable)?;
402
403            // Gradient
404            gradients[param_idx] = (expectation_plus - expectation_minus) / 2.0;
405        }
406
407        Ok(gradients)
408    }
409
410    /// Finite difference gradient computation
411    fn finite_difference_gradient(
412        &self,
413        observable: &PauliOperatorSum,
414        params: &[f64],
415        step_size: f64,
416    ) -> Result<Vec<f64>> {
417        let mut gradients = vec![0.0; self.num_parameters];
418
419        for param_idx in 0..self.num_parameters {
420            // Forward difference
421            let mut params_plus = params.to_vec();
422            params_plus[param_idx] += step_size;
423            let state_plus = self.evaluate(&params_plus)?;
424            let expectation_plus = compute_expectation_value(&state_plus, observable)?;
425
426            // Current value
427            let state = self.evaluate(params)?;
428            let expectation = compute_expectation_value(&state, observable)?;
429
430            // Gradient
431            gradients[param_idx] = (expectation_plus - expectation) / step_size;
432        }
433
434        Ok(gradients)
435    }
436
437    /// SPSA gradient estimation
438    fn spsa_gradient(
439        &self,
440        observable: &PauliOperatorSum,
441        params: &[f64],
442        step_size: f64,
443    ) -> Result<Vec<f64>> {
444        // Generate random perturbation vector
445        let mut perturbation = vec![0.0; self.num_parameters];
446        for p in &mut perturbation {
447            *p = if fastrand::bool() { 1.0 } else { -1.0 };
448        }
449
450        // Two evaluations with opposite perturbations
451        let mut params_plus = params.to_vec();
452        let mut params_minus = params.to_vec();
453
454        for i in 0..self.num_parameters {
455            params_plus[i] += step_size * perturbation[i];
456            params_minus[i] -= step_size * perturbation[i];
457        }
458
459        let state_plus = self.evaluate(&params_plus)?;
460        let state_minus = self.evaluate(&params_minus)?;
461
462        let expectation_plus = compute_expectation_value(&state_plus, observable)?;
463        let expectation_minus = compute_expectation_value(&state_minus, observable)?;
464
465        // SPSA gradient estimate
466        let diff = (expectation_plus - expectation_minus) / (2.0 * step_size);
467        let gradients = perturbation.iter().map(|&p| diff / p).collect();
468
469        Ok(gradients)
470    }
471}
472
473/// VQE algorithm with automatic differentiation
474pub struct VQEWithAutodiff {
475    /// Parametric ansatz circuit
476    pub ansatz: ParametricCircuit,
477    /// Hamiltonian observable
478    pub hamiltonian: PauliOperatorSum,
479    /// Autodiff context
480    pub context: AutoDiffContext,
481    /// Optimization history
482    pub history: Vec<VQEIteration>,
483    /// Convergence criteria
484    pub convergence: ConvergenceCriteria,
485}
486
487/// Single VQE iteration data
488#[derive(Debug, Clone)]
489pub struct VQEIteration {
490    /// Iteration number
491    pub iteration: usize,
492    /// Parameter values
493    pub parameters: Vec<f64>,
494    /// Energy expectation value
495    pub energy: f64,
496    /// Gradient norm
497    pub gradient_norm: f64,
498    /// Function evaluations so far
499    pub func_evals: usize,
500    /// Gradient evaluations so far
501    pub grad_evals: usize,
502}
503
504/// Convergence criteria for VQE
505#[derive(Debug, Clone)]
506pub struct ConvergenceCriteria {
507    /// Maximum iterations
508    pub max_iterations: usize,
509    /// Energy tolerance
510    pub energy_tolerance: f64,
511    /// Gradient norm tolerance
512    pub gradient_tolerance: f64,
513    /// Maximum function evaluations
514    pub max_func_evals: usize,
515}
516
517impl Default for ConvergenceCriteria {
518    fn default() -> Self {
519        Self {
520            max_iterations: 1000,
521            energy_tolerance: 1e-6,
522            gradient_tolerance: 1e-6,
523            max_func_evals: 10000,
524        }
525    }
526}
527
528impl VQEWithAutodiff {
529    /// Create new VQE instance
530    pub fn new(
531        ansatz: ParametricCircuit,
532        hamiltonian: PauliOperatorSum,
533        initial_params: Vec<f64>,
534        gradient_method: GradientMethod,
535    ) -> Self {
536        let context = AutoDiffContext::new(initial_params, gradient_method);
537
538        Self {
539            ansatz,
540            hamiltonian,
541            context,
542            history: Vec::new(),
543            convergence: ConvergenceCriteria::default(),
544        }
545    }
546
547    /// Set convergence criteria
548    pub fn with_convergence(mut self, convergence: ConvergenceCriteria) -> Self {
549        self.convergence = convergence;
550        self
551    }
552
553    /// Evaluate energy for current parameters
554    pub fn evaluate_energy(&mut self) -> Result<f64> {
555        let state = self.ansatz.evaluate(&self.context.parameters)?;
556        let energy = compute_expectation_value(&state, &self.hamiltonian)?;
557        self.context.func_evaluations += 1;
558        Ok(energy)
559    }
560
561    /// Compute gradient for current parameters
562    pub fn compute_gradient(&mut self) -> Result<Vec<f64>> {
563        let gradients = self.ansatz.gradient_expectation(
564            &self.hamiltonian,
565            &self.context.parameters,
566            self.context.method,
567        )?;
568        self.context.gradients = gradients.clone();
569        self.context.grad_evaluations += 1;
570        Ok(gradients)
571    }
572
573    /// Perform one VQE optimization step using gradient descent
574    pub fn step(&mut self, learning_rate: f64) -> Result<VQEIteration> {
575        let energy = self.evaluate_energy()?;
576        let gradients = self.compute_gradient()?;
577
578        // Gradient descent update
579        for (i, &grad) in gradients.iter().enumerate() {
580            self.context.parameters[i] -= learning_rate * grad;
581        }
582
583        let gradient_norm = gradients.iter().map(|g| g * g).sum::<f64>().sqrt();
584
585        let iteration = VQEIteration {
586            iteration: self.history.len(),
587            parameters: self.context.parameters.clone(),
588            energy,
589            gradient_norm,
590            func_evals: self.context.func_evaluations,
591            grad_evals: self.context.grad_evaluations,
592        };
593
594        self.history.push(iteration.clone());
595        Ok(iteration)
596    }
597
598    /// Run VQE optimization until convergence
599    pub fn optimize(&mut self, learning_rate: f64) -> Result<VQEResult> {
600        while !self.is_converged()? {
601            let iteration = self.step(learning_rate)?;
602
603            if iteration.iteration >= self.convergence.max_iterations {
604                break;
605            }
606
607            if iteration.func_evals >= self.convergence.max_func_evals {
608                break;
609            }
610        }
611
612        let final_iteration = self.history.last().unwrap();
613
614        Ok(VQEResult {
615            optimal_parameters: final_iteration.parameters.clone(),
616            optimal_energy: final_iteration.energy,
617            iterations: self.history.len(),
618            converged: self.is_converged()?,
619            history: self.history.clone(),
620        })
621    }
622
623    /// Check convergence
624    fn is_converged(&self) -> Result<bool> {
625        if self.history.len() < 2 {
626            return Ok(false);
627        }
628
629        let current = &self.history[self.history.len() - 1];
630        let previous = &self.history[self.history.len() - 2];
631
632        let energy_converged =
633            (current.energy - previous.energy).abs() < self.convergence.energy_tolerance;
634        let gradient_converged = current.gradient_norm < self.convergence.gradient_tolerance;
635
636        Ok(energy_converged && gradient_converged)
637    }
638}
639
640/// VQE optimization result
641#[derive(Debug, Clone)]
642pub struct VQEResult {
643    /// Optimal parameters found
644    pub optimal_parameters: Vec<f64>,
645    /// Optimal energy value
646    pub optimal_energy: f64,
647    /// Number of iterations performed
648    pub iterations: usize,
649    /// Whether optimization converged
650    pub converged: bool,
651    /// Full optimization history
652    pub history: Vec<VQEIteration>,
653}
654
655// Helper functions
656
657/// Compute expectation value of observable for given state
658fn compute_expectation_value(
659    state: &Array1<Complex64>,
660    observable: &PauliOperatorSum,
661) -> Result<f64> {
662    let mut expectation = 0.0;
663
664    for term in &observable.terms {
665        // Compute ⟨ψ|P|ψ⟩ for each Pauli string P
666        let pauli_expectation = compute_pauli_expectation_from_state(state, term)?;
667        expectation += term.coefficient.re * pauli_expectation.re;
668    }
669
670    Ok(expectation)
671}
672
673/// Compute expectation value of a single Pauli string
674fn compute_pauli_expectation_from_state(
675    state: &Array1<Complex64>,
676    pauli_string: &PauliString,
677) -> Result<Complex64> {
678    let num_qubits = pauli_string.num_qubits;
679    let dim = 1 << num_qubits;
680    let mut result = Complex64::new(0.0, 0.0);
681
682    for (i, &amplitude) in state.iter().enumerate() {
683        if i >= dim {
684            break;
685        }
686
687        // Apply Pauli string to basis state |i⟩
688        let mut coeff = Complex64::new(1.0, 0.0);
689        let mut target_state = i;
690
691        for (qubit, &pauli_op) in pauli_string.operators.iter().enumerate() {
692            let bit = (i >> qubit) & 1;
693
694            use crate::pauli::PauliOperator;
695            match pauli_op {
696                PauliOperator::I => {} // Identity does nothing
697                PauliOperator::X => {
698                    // X flips the bit
699                    target_state ^= 1 << qubit;
700                }
701                PauliOperator::Y => {
702                    // Y flips the bit and adds phase
703                    target_state ^= 1 << qubit;
704                    coeff *= if bit == 0 {
705                        Complex64::new(0.0, 1.0)
706                    } else {
707                        Complex64::new(0.0, -1.0)
708                    };
709                }
710                PauliOperator::Z => {
711                    // Z adds phase based on bit value
712                    if bit == 1 {
713                        coeff *= Complex64::new(-1.0, 0.0);
714                    }
715                }
716            }
717        }
718
719        if target_state < dim {
720            result += amplitude.conj() * coeff * state[target_state];
721        }
722    }
723
724    Ok(result * pauli_string.coefficient)
725}
726
727/// Convenience functions for creating common ansätze
728pub mod ansatze {
729    use super::*;
730
731    /// Create a hardware-efficient ansatz
732    pub fn hardware_efficient(num_qubits: usize, num_layers: usize) -> ParametricCircuit {
733        let mut circuit = ParametricCircuit::new(num_qubits);
734        let mut param_idx = 0;
735
736        for layer in 0..num_layers {
737            // Single-qubit rotations
738            for qubit in 0..num_qubits {
739                let _ = circuit.ry(qubit, param_idx);
740                param_idx += 1;
741                let _ = circuit.rz(qubit, param_idx);
742                param_idx += 1;
743            }
744
745            // Entangling layer (would need CNOT gates - simplified here)
746            // In practice, would add parametric CNOT gates
747        }
748
749        circuit
750    }
751
752    /// Create a QAOA ansatz for MaxCut problem
753    pub fn qaoa_maxcut(
754        num_qubits: usize,
755        num_layers: usize,
756        edges: &[(usize, usize)],
757    ) -> ParametricCircuit {
758        let mut circuit = ParametricCircuit::new(num_qubits);
759        let mut param_idx = 0;
760
761        // Initial superposition
762        for qubit in 0..num_qubits {
763            let _ = circuit.ry(qubit, param_idx); // RY(π/2) for H gate equivalent
764        }
765
766        for _layer in 0..num_layers {
767            // Problem Hamiltonian evolution (ZZ terms)
768            for &(i, j) in edges {
769                // Would implement ZZ rotation here
770                // For now, approximate with RZ gates
771                let _ = circuit.rz(i, param_idx);
772                let _ = circuit.rz(j, param_idx);
773                param_idx += 1;
774            }
775
776            // Mixer Hamiltonian evolution (X terms)
777            for qubit in 0..num_qubits {
778                let _ = circuit.rx(qubit, param_idx);
779                param_idx += 1;
780            }
781        }
782
783        circuit
784    }
785}
786
787#[cfg(test)]
788mod tests {
789    use super::*;
790
791    #[test]
792    fn test_parametric_rx_matrix() {
793        let rx_gate = ParametricRX {
794            qubit: 0,
795            param_idx: 0,
796        };
797        let params = vec![PI / 2.0];
798
799        let matrix = rx_gate.matrix(&params).unwrap();
800
801        // RX(π/2) should be approximately [[1/√2, -i/√2], [-i/√2, 1/√2]]
802        let expected_val = 1.0 / 2.0_f64.sqrt();
803        assert!((matrix[[0, 0]].re - expected_val).abs() < 1e-10);
804        assert!((matrix[[0, 1]].im + expected_val).abs() < 1e-10);
805    }
806
807    #[test]
808    fn test_autodiff_context() {
809        let params = vec![1.0, 2.0, 3.0];
810        let mut context = AutoDiffContext::new(params.clone(), GradientMethod::ParameterShift);
811
812        assert_eq!(context.parameters, params);
813        assert_eq!(context.gradients.len(), 3);
814
815        context.update_parameters(vec![4.0, 5.0, 6.0]);
816        assert_eq!(context.parameters, vec![4.0, 5.0, 6.0]);
817    }
818
819    #[test]
820    fn test_parametric_circuit_creation() {
821        let mut circuit = ParametricCircuit::new(2);
822        let _ = circuit.rx(0, 0);
823        let _ = circuit.ry(1, 1);
824
825        assert_eq!(circuit.gates.len(), 2);
826        assert_eq!(circuit.num_parameters, 2);
827    }
828
829    #[test]
830    fn test_hardware_efficient_ansatz() {
831        let ansatz = ansatze::hardware_efficient(3, 2);
832        assert_eq!(ansatz.num_qubits, 3);
833        assert!(ansatz.num_parameters > 0);
834    }
835}